SpringBoot + Spring Security + Thymeleaf 实现权限管理登录

本文通过一个登录的例子介绍 SpringBoot + Spring Security + Thymeleaf 权限管理。

 

一、数据库

用户登录账号是 admin,saysky,lockeduser

密码都是 123456

 

1、表结构

user 表

SpringBoot + Spring Security + Thymeleaf 实现权限管理登录

 

authority 表

SpringBoot + Spring Security + Thymeleaf 实现权限管理登录

 

user_authority 表

SpringBoot + Spring Security + Thymeleaf 实现权限管理登录

 

 

2、数据

user 表

SpringBoot + Spring Security + Thymeleaf 实现权限管理登录

 

authority 表

SpringBoot + Spring Security + Thymeleaf 实现权限管理登录

 

user_authority 表

SpringBoot + Spring Security + Thymeleaf 实现权限管理登录

 

3、SQL 代码

  1. SET NAMES utf8;
  2. SET FOREIGN_KEY_CHECKS = 0;
  3. -- ----------------------------
  4. --  Table structure for `authority`
  5. -- ----------------------------
  6. DROP TABLE IF EXISTS `authority`;
  7. CREATE TABLE `authority` (
  8.   `id` bigint(20) NOT NULL AUTO_INCREMENT,
  9.   `namevarchar(255) NOT NULL,
  10.   PRIMARY KEY (`id`)
  11. ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
  12. -- ----------------------------
  13. --  Records of `authority`
  14. -- ----------------------------
  15. BEGIN;
  16. INSERT INTO `authority` VALUES ('1', 'ROLE_ADMIN'), ('2', 'ROLE_USER');
  17. COMMIT;
  18. -- ----------------------------
  19. --  Table structure for `user`
  20. -- ----------------------------
  21. DROP TABLE IF EXISTS `user`;
  22. CREATE TABLE `user` (
  23.   `id` bigint(20) NOT NULL AUTO_INCREMENT,
  24.   `username` varchar(20) NOT NULL,
  25.   `passwordvarchar(100) NOT NULL,
  26.   `namevarchar(20) NOT NULL,
  27.   `email` varchar(50) NOT NULL,
  28.   `avatar` varchar(200) DEFAULT NULL,
  29.   `create_time` datetime DEFAULT NULL,
  30.   `last_login_time` datetime DEFAULT NULL,
  31.   `status` varchar(10) DEFAULT NULL,
  32.   PRIMARY KEY (`id`),
  33.   UNIQUE KEY `UK_ob8kqyqqgmefl0aco34akdtpe` (`email`),
  34.   UNIQUE KEY `UK_sb8bbouer5wak8vyiiy4pf2bx` (`username`)
  35. ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
  36. -- ----------------------------
  37. --  Records of `user`
  38. -- ----------------------------
  39. BEGIN;
  40. INSERT INTO `userVALUES ('1', 'admin', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVKIUi', '管理员', 'admin@liuyanzhao.com', nullnullnull, 'normal'), ('2', 'saysky', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVKIUi', '言曌', '847064370@qq.com', nullnullnull, 'normal'), ('3', 'lockuser', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVKIUi', '锁定账号', 'locked@qq.com', nullnullnull, 'locked');
  41. COMMIT;
  42. -- ----------------------------
  43. --  Table structure for `user_authority`
  44. -- ----------------------------
  45. DROP TABLE IF EXISTS `user_authority`;
  46. CREATE TABLE `user_authority` (
  47.   `user_id` bigint(20) NOT NULL,
  48.   `authority_id` bigint(20) NOT NULL,
  49.   KEY `FKgvxjs381k6f48d5d2yi11uh89` (`authority_id`),
  50.   KEY `FKpqlsjpkybgos9w2svcri7j8xy` (`user_id`),
  51.   CONSTRAINT `FKgvxjs381k6f48d5d2yi11uh89` FOREIGN KEY (`authority_id`) REFERENCES `authority` (`id`),
  52.   CONSTRAINT `FKpqlsjpkybgos9w2svcri7j8xy` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`)
  53. ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  54. -- ----------------------------
  55. --  Records of `user_authority`
  56. -- ----------------------------
  57. BEGIN;
  58. INSERT INTO `user_authority` VALUES ('1', '1'), ('2', '2'), ('1', '2');
  59. COMMIT;
  60. SET FOREIGN_KEY_CHECKS = 1;

 

二、Maven 依赖

pom.xml

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3.          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  4.     <modelVersion>4.0.0</modelVersion>
  5.     <groupId>com.liuyanzhao</groupId>
  6.     <artifactId>chuyun</artifactId>
  7.     <version>0.0.1-SNAPSHOT</version>
  8.     <packaging>war</packaging>
  9.     <name>chuyun</name>
  10.     <description>Chuyun Blog for Spring Boot</description>
  11.     <parent>
  12.         <groupId>org.springframework.boot</groupId>
  13.         <artifactId>spring-boot-starter-parent</artifactId>
  14.         <version>1.5.9.RELEASE</version>
  15.         <relativePath/> <!-- lookup parent from repository -->
  16.     </parent>
  17.     <properties>
  18.         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  19.         <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
  20.         <java.version>1.8</java.version>
  21.         <thymeleaf.version>3.0.3.RELEASE</thymeleaf.version>
  22.         <thymeleaf-layout-dialect.version>2.0.0</thymeleaf-layout-dialect.version>
  23.         <elasticsearch.version>2.4.4</elasticsearch.version>
  24.     </properties>
  25.     <dependencies>
  26.         <dependency>
  27.             <groupId>org.springframework.boot</groupId>
  28.             <artifactId>spring-boot-starter-web</artifactId>
  29.         </dependency>
  30.         <!--lombok-->
  31.         <dependency>
  32.             <groupId>org.projectlombok</groupId>
  33.             <artifactId>lombok</artifactId>
  34.             <optional>true</optional>
  35.         </dependency>
  36.         <dependency>
  37.             <groupId>org.springframework.boot</groupId>
  38.             <artifactId>spring-boot-starter-test</artifactId>
  39.             <scope>test</scope>
  40.         </dependency>
  41.         <!-- Thymeleaf -->
  42.         <dependency>
  43.             <groupId>org.springframework.boot</groupId>
  44.             <artifactId>spring-boot-starter-thymeleaf</artifactId>
  45.         </dependency>
  46.         <!-- Spring Data JPA-->
  47.         <dependency>
  48.             <groupId>org.springframework.boot</groupId>
  49.             <artifactId>spring-boot-starter-data-jpa</artifactId>
  50.         </dependency>
  51.         <!-- mysql-->
  52.         <dependency>
  53.             <groupId>mysql</groupId>
  54.             <artifactId>mysql-connector-java</artifactId>
  55.         </dependency>
  56.         <!-- 热部署 -->
  57.         <dependency>
  58.             <groupId>org.springframework.boot</groupId>
  59.             <artifactId>spring-boot-devtools</artifactId>
  60.             <optional>true</optional>
  61.         </dependency>
  62.         <!-- Spring Boot Elasticsearch 依赖 -->
  63.         <dependency>
  64.             <groupId>org.springframework.boot</groupId>
  65.             <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
  66.         </dependency>
  67.         <dependency>
  68.             <groupId>net.java.dev.jna</groupId>
  69.             <artifactId>jna</artifactId>
  70.             <version>4.3.0</version>
  71.         </dependency>
  72.         <!-- Spring Security 依赖 -->
  73.         <dependency>
  74.             <groupId>org.springframework.boot</groupId>
  75.             <artifactId>spring-boot-starter-security</artifactId>
  76.         </dependency>
  77.         <dependency>
  78.             <groupId>org.thymeleaf.extras</groupId>
  79.             <artifactId>thymeleaf-extras-springsecurity4</artifactId>
  80.             <version>3.0.2.RELEASE</version>
  81.         </dependency>
  82.         <!--fasthson-->
  83.         <dependency>
  84.             <groupId>com.alibaba</groupId>
  85.             <artifactId>fastjson</artifactId>
  86.             <version>1.2.7</version>
  87.         </dependency>
  88.         <!--验证码kaptcha-->
  89.         <dependency>
  90.             <groupId>com.github.penggle</groupId>
  91.             <artifactId>kaptcha</artifactId>
  92.             <version>2.3.2</version>
  93.         </dependency>
  94.     </dependencies>
  95.     <build>
  96.         <plugins>
  97.             <plugin>
  98.                 <groupId>org.springframework.boot</groupId>
  99.                 <artifactId>spring-boot-maven-plugin</artifactId>
  100.                 <configuration>
  101.                     <fork>true</fork>
  102.                 </configuration>
  103.             </plugin>
  104.         </plugins>
  105.     </build>
  106. </project>

主要需要 SpringBootThymeleaf、Spring Security 的依赖

 

三、实体类

User.java

  1. package com.liuyanzhao.chuyun.entity;
  2. import lombok.Data;
  3. import org.hibernate.validator.constraints.Email;
  4. import org.hibernate.validator.constraints.NotEmpty;
  5. import org.springframework.security.core.GrantedAuthority;
  6. import org.springframework.security.core.authority.SimpleGrantedAuthority;
  7. import org.springframework.security.core.userdetails.UserDetails;
  8. import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  9. import org.springframework.security.crypto.password.PasswordEncoder;
  10. import javax.persistence.*;
  11. import javax.validation.constraints.Size;
  12. import java.io.Serializable;
  13. import java.util.ArrayList;
  14. import java.util.Collection;
  15. import java.util.Date;
  16. import java.util.List;
  17. /**
  18.  * @author 言曌
  19.  * @date 2017/12/28 上午9:06
  20.  */
  21. @Entity // 实体
  22. @Data
  23. public class User implements UserDetails, Serializable {
  24.     private static final long serialVersionUID = 1L;
  25.     @Id // 主键
  26.     @GeneratedValue(strategy = GenerationType.IDENTITY) // 自增长策略
  27.     private Long id; // 用户的唯一标识
  28.     @NotEmpty(message = "昵称不能为空")
  29.     @Size(min=2, max=20)
  30.     @Column(nullable = false, length = 20// 映射为字段,值不能为空
  31.     private String name;
  32.     @NotEmpty(message = "邮箱不能为空")
  33.     @Size(max=50)
  34.     @Email(message= "邮箱格式不对" )
  35.     @Column(nullable = false, length = 50, unique = true)
  36.     private String email;
  37.     @NotEmpty(message = "账号不能为空")
  38.     @Size(min=3, max=20)
  39.     @Column(nullable = false, length = 20, unique = true)
  40.     private String username; // 用户账号,用户登录时的唯一标识
  41.     @NotEmpty(message = "密码不能为空")
  42.     @Size(max=100)
  43.     @Column(length = 100)
  44.     private String password; // 登录时密码
  45.     @Column(length = 200)
  46.     private String avatar; // 头像图片地址
  47.     private Date createTime;
  48.     private Date lastLoginTime;
  49.     @Column(length = 10)
  50.     private String status;
  51.     @ManyToMany(cascade = CascadeType.DETACH, fetch = FetchType.EAGER)
  52.     @JoinTable(name = "user_authority", joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
  53.             inverseJoinColumns = @JoinColumn(name = "authority_id", referencedColumnName = "id"))
  54.     private List<Authority> authorities;
  55.     protected User() { // JPA 的规范要求无参构造函数;设为 protected 防止直接使用
  56.     }
  57.     public User(String name, String email,String username,String password) {
  58.         this.name = name;
  59.         this.email = email;
  60.         this.username = username;
  61.         this.password = password;
  62.     }
  63.     public Collection<? extends GrantedAuthority> getAuthorities() {
  64.         //  需将 List<Authority> 转成 List<SimpleGrantedAuthority>,否则前端拿不到角色列表名称
  65.         List<SimpleGrantedAuthority> simpleAuthorities = new ArrayList<>();
  66.         for(GrantedAuthority authority : this.authorities){
  67.             simpleAuthorities.add(new SimpleGrantedAuthority(authority.getAuthority()));
  68.         }
  69.         return simpleAuthorities;
  70.     }
  71.     public void setAuthorities(List<Authority> authorities) {
  72.         this.authorities = authorities;
  73.     }
  74.     public void setEncodePassword(String password) {
  75.         PasswordEncoder  encoder = new BCryptPasswordEncoder();
  76.         String encodePasswd = encoder.encode(password);
  77.         this.password = encodePasswd;
  78.     }
  79.     @Override
  80.     public boolean isAccountNonExpired() {
  81.         return true;
  82.     }
  83.     @Override
  84.     public boolean isAccountNonLocked() {
  85.         return true;
  86.     }
  87.     @Override
  88.     public boolean isCredentialsNonExpired() {
  89.         return true;
  90.     }
  91.     @Override
  92.     public boolean isEnabled() {
  93.         return true;
  94.     }
  95. }

 

Authority.java

  1. package com.liuyanzhao.chuyun.entity;
  2. import org.springframework.security.core.GrantedAuthority;
  3. import javax.persistence.*;
  4. /**
  5.  * 权限
  6.  * @author 言曌
  7.  * @date 2018/1/26 下午2:05
  8.  */
  9. @Entity
  10. public class Authority implements GrantedAuthority {
  11.     private static final long serialVersionUID = 1L;
  12.     @Id // 主键
  13.     @GeneratedValue(strategy = GenerationType.IDENTITY) // 自增长策略
  14.     private Long id; // 用户的唯一标识
  15.     @Column(nullable = false// 映射为字段,值不能为空
  16.     private String name;
  17.     public Long getId() {
  18.         return id;
  19.     }
  20.     public void setId(Long id) {
  21.         this.id = id;
  22.     }
  23.     @Override
  24.     public String getAuthority() {
  25.         return name;
  26.     }
  27.     public void setName(String name) {
  28.         this.name = name;
  29.     }
  30. }

 

四、Dao 层

UserRepository.java

  1. package com.liuyanzhao.chuyun.repository;
  2. import com.liuyanzhao.chuyun.entity.User;
  3. import org.springframework.data.jpa.repository.JpaRepository;
  4. /**
  5.  * 用户repository
  6.  * @author 言曌
  7.  * @date 2017/12/27 0027 20:50
  8.  */
  9. public interface UserRepository extends JpaRepository<User, Long> {
  10.     /**
  11.      * 根据用户名查找用户
  12.      * @param username
  13.      * @return
  14.      */
  15.     User findByUsername(String username);
  16. }

 

 

五、Service 层

CustomUserService.java

  1. package com.liuyanzhao.chuyun.service;
  2. import com.liuyanzhao.chuyun.entity.User;
  3. import com.liuyanzhao.chuyun.repository.UserRepository;
  4. import org.springframework.beans.factory.annotation.Autowired;
  5. import org.springframework.security.authentication.LockedException;
  6. import org.springframework.security.core.userdetails.UserDetailsService;
  7. import org.springframework.security.core.userdetails.UsernameNotFoundException;
  8. import org.springframework.stereotype.Service;
  9. /**
  10.  * @author 言曌
  11.  * @date 2018/1/30 下午8:37
  12.  */
  13. @Service
  14. public class CustomUserService implements UserDetailsService{
  15.     @Autowired
  16.     private UserRepository userRepository;
  17.     @Override
  18.     public User loadUserByUsername(String username) throws UsernameNotFoundException {
  19.         User user = userRepository.findByUsername(username);
  20.         if (user == null) {
  21.             throw new UsernameNotFoundException("用户名不存在");
  22.         } else if("locked".equals(user.getStatus())) { //被锁定,无法登录
  23.             throw new LockedException("用户被锁定");
  24.         }
  25.         return user;
  26.     }
  27. }

 

六、Spring Security 配置

SecurityConfig.java

  1. package com.liuyanzhao.chuyun.config;
  2. import com.liuyanzhao.chuyun.service.CustomUserService;
  3. import org.springframework.beans.factory.annotation.Autowired;
  4. import org.springframework.context.annotation.Bean;
  5. import org.springframework.security.authentication.AuthenticationProvider;
  6. import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
  7. import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
  8. import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
  9. import org.springframework.security.config.annotation.web.builders.HttpSecurity;
  10. import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
  11. import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
  12. import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  13. import org.springframework.security.crypto.password.PasswordEncoder;
  14. /**
  15.  * 安全配置类
  16.  *
  17.  * @author 言曌
  18.  * @date 2018/1/23 上午11:37
  19.  */
  20. @EnableWebSecurity
  21. @EnableGlobalMethodSecurity(prePostEnabled = true// 启用方法安全设置
  22. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  23.     private static final String KEY = "liuyanzhao.com";
  24.     @Autowired
  25.     private PasswordEncoder passwordEncoder;
  26.     @Bean
  27.     CustomUserService customUserService() {
  28.         return new CustomUserService();
  29.     }
  30.     @Bean
  31.     public PasswordEncoder passwordEncoder() {
  32.         return new BCryptPasswordEncoder();   // 使用 BCrypt 加密
  33.     }
  34.     @Bean
  35.     public AuthenticationProvider authenticationProvider() {
  36.         DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
  37.         authenticationProvider.setUserDetailsService(customUserService());
  38.         authenticationProvider.setPasswordEncoder(passwordEncoder); // 设置密码加密方式
  39.         return authenticationProvider;
  40.     }
  41.     /**
  42.      * 自定义配置
  43.      */
  44.     @Override
  45.     protected void configure(HttpSecurity http) throws Exception {
  46.         http.authorizeRequests().antMatchers("/","/css/**""/js/**", "/fonts/**","/users").permitAll() // 都可以访问
  47.                 .antMatchers("/h2-console/**").permitAll() // 都可以访问
  48.                 .antMatchers("/admin/**").hasRole("ADMIN"// 需要相应的角色才能访问
  49.                 .antMatchers("/console/**").hasAnyRole("ADMIN","USER"// 需要相应的角色才能访问
  50.                 .and()
  51.                 .formLogin()   //基于 Form 表单登录验证
  52.                 .loginPage("/login").failureUrl("/login?error=true"// 自定义登录界面
  53.                 .and().rememberMe().key(KEY) // 启用 remember me
  54.                 .and().exceptionHandling().accessDeniedPage("/403");  // 处理异常,拒绝访问就重定向到 403 页面
  55.         http.csrf().ignoringAntMatchers("/h2-console/**"); // 禁用 H2 控制台的 CSRF 防护
  56.         http.headers().frameOptions().sameOrigin(); // 允许来自同一来源的H2 控制台的请求
  57.     }
  58.     /**
  59.      * 认证信息管理
  60.      *
  61.      * @param auth
  62.      * @throws Exception
  63.      */
  64.     @Autowired
  65.     public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
  66.         //auth.userDetailsService(userDetailsService);
  67.         auth.userDetailsService(customUserService());
  68.         auth.authenticationProvider(authenticationProvider());
  69.     }
  70. }

 

七、HTML 页面

首页:http://localhost:8080

登录页面:http://localhost:8080/login

登录错误页面:http://localhost:8080/login?error=true

退出登录:http://localhost:8080/logout

 

index.html

  1. <!DOCTYPE html>
  2. <html xmlns="http://www.w3.org/1999/xhtml"
  3.       xmlns:th="http://www.thymeleaf.org"
  4.       xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
  5. <head>
  6.     <meta charset="UTF-8">
  7.     <meta name="viewport"
  8.           content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
  9. </head>
  10. <body>
  11. <!--匿名-->
  12. <div sec:authorize="isAnonymous()">
  13.     未登录,点击 <a th:href="@{/login}">登录</a>
  14. </div>
  15. <!--登录-->
  16. <div sec:authorize="isAuthenticated()">
  17.     <p>已登录</p>
  18.     <p>登录名:<span sec:authentication="name"></span></p>
  19.     <p>角色:<span sec:authentication="principal.authorities"></span></p>
  20.     <p>Username:<span sec:authentication="principal.username"></span></p>
  21.     <p>Password:<span sec:authentication="principal.password"></span></p>
  22.     <p>Email :<span sec:authentication="principal.email"></span></p>
  23.     <p>Name:<span sec:authentication="principal.name"></span></p>
  24.     <p>Status:<span sec:authentication="principal.status"></span></p>
  25.     <p>拥有的角色:
  26.     <span sec:authorize="hasRole('ROLE_ADMIN')">管理员</span>
  27.     <span sec:authorize="hasRole('ROLE_USER')">用户</span>
  28.     </p>
  29. </div>
  30. </body>
  31. </html>

 

login.html

  1. <!DOCTYPE html>
  2. <html xmlns="http://www.w3.org/1999/xhtml"
  3.       xmlns:th="http://www.thymeleaf.org">
  4. <head>
  5.     <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
  6.     <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
  7.     <title>登录页面</title>
  8. <body>
  9. <form th:action="@{login}" method="post" id="loginForm">
  10.     <span th:if="${param.error}" th:text="${session.SPRING_SECURITY_LAST_EXCEPTION.message}"></span>
  11.     用户名:<input type="text" name="username" class="username" id="username" placeholder="用户名" autocomplete="off"/> <br>
  12.     密 码:<input type="password" name="password" class="password" id="password" placeholder="密码"
  13.                 oncontextmenu="return false"
  14.                 onpaste="return false"/> <br>
  15.     <input type="checkbox" name="remember-me"/>记住我 <br>
  16.     <input id="submit" type="submit" value="登录"/>
  17. </form>
  18. </body>
  19. </html>

 

八、运行效果

1、访问首页:http://localhost:8080

SpringBoot + Spring Security + Thymeleaf 实现权限管理登录

 

2、点击 登录,填写登录信息,不勾选记住我

账号:admin,密码:123456

SpringBoot + Spring Security + Thymeleaf 实现权限管理登录

 

3、登录成功,跳转到首页,显示登录信息

SpringBoot + Spring Security + Thymeleaf 实现权限管理登录

 

4、访问 http://localhost:8080/logout,可退出登录

SpringBoot + Spring Security + Thymeleaf 实现权限管理登录

 

5、登录错误显示

SpringBoot + Spring Security + Thymeleaf 实现权限管理登录

 

这里,登录失败的话(用户名不存在或者密码错误),都是显示Bad credentials

SpringBoot + Spring Security + Thymeleaf 实现权限管理登录

 

如果登录被锁定的用户,如用户名为lockeduser

SpringBoot + Spring Security + Thymeleaf 实现权限管理登录

 

 

6、记住我也是有效的,关闭浏览器后,下次启动,依然是登录状态

记住密码后,我们可以看到浏览器里添加了 一条 cookie

SpringBoot + Spring Security + Thymeleaf 实现权限管理登录

 

 

本文地址:https://liuyanzhao.com/7431.html

  • 微信
  • 交流学习,有偿服务
  • weinxin
  • 博客/Java交流群
  • 资源分享,问题解决,技术交流。群号:590480292
  • weinxin
言曌

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

目前评论:1   其中:访客  1   博主  0

    • avatar facer

      支持一下!