一、前言

SpringBoot+Shiro+Mybatis完成的。

之前看了一位小伙伴的Shiro教程,跟着做了,遇到蛮多坑的(´இ皿இ`)

修改整理了一下,成功跑起来了。可以通过postman进行测试

不多比比∠( ᐛ 」∠)_,直接上源码:https://github.com/niaobulashi/spring-boot-learning/tree/master/spring-boot-20-shiro

二、Shiro是啥

Apache Shiro是一个功能强大、灵活的、开源的安全框架。可以干净利落地处理身份验证、授权、企业会话管理和加密。

二、Shiro可以干啥

  • 验证用户身份
  • 用户访问权限控制,比如:1、判断用户是否分配了一定的安全角色。2、判断用户是否被授予完成某个操作的权限
  • 在非 Web 或 EJB 容器的环境下可以任意使用 Session API
  • 可以响应认证、访问控制,或者 Session 生命周期中发生的事件
  • 可将一个或以上用户安全数据源数据组合成一个复合的用户 “view”(视图)
  • 支持单点登录(SSO)功能
  • 支持提供“Remember Me”服务,获取用户关联信息而无需登录

Shiro框架图如下:

  • Authentication(认证):用户身份识别,通常被称为用户“登录”
  • Authorization(授权):访问控制。比如某个用户是否具有某个操作的使用权限。
  • Session Management(会话管理):特定于用户的会话管理,甚至在非web 或 EJB 应用程序。
  • Cryptography(加密):在对数据源使用加密算法加密的同时,保证易于使用。

在概念层,Shiro架构包含三个主要的理念:Subject,SecurityManager和 Realm。下面的图展示了这些组件如何相互作用,我们将在下面依次对其进行描述。

  • Subject:当前用户,Subject 可以是一个人,但也可以是第三方服务、守护进程帐户、时钟守护任务或者其它–当前和软件交互的任何事件。
  • SecurityManager:管理所有Subject,SecurityManager 是 Shiro 架构的核心,配合内部安全组件共同组成安全伞。
  • Realms:用于进行权限信息的验证,我们自己实现。Realm 本质上是一个特定的安全 DAO:它封装与数据源连接的细节,得到Shiro 所需的相关的数据。在配置 Shiro 的时候,你必须指定至少一个Realm 来实现认证(authentication)和/或授权(authorization)。

三、代码实现

1、添加Maven依赖

  1. <dependency>
  2. <groupId>org.apache.shiro</groupId>
  3. <artifactId>shiro-spring</artifactId>
  4. <version>1.4.1</version>

2、配置文件

application.yml

  1. # 服务器端口
  2. server:
  3. port: 8081
  4. # 配置Spring相关信息
  5. spring:
  6. datasource:
  7. driver-class-name: com.mysql.jdbc.Driver
  8. url: jdbc:mysql://localhost:3306/test?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
  9. username: root
  10. password: root
  11. # 配置Mybatis
  12. mybatis:
  13. type-aliases-package: com.niaobulashi.model
  14. mapper-locations: classpath:mapper/*.xml
  15. configuration:
  16. # 开启驼峰命名转换
  17. map-underscore-to-camel-case: true
  18. # 打印SQL日志
  19. logging:
  20. level:
  21. com.niaobulashi.mapper: DEBUG

启动方法添加mapper扫描,我一般都是在启动方法上面声明,否则需要在每一个mapper上单独声明扫描

  1. @SpringBootApplication
  2. @MapperScan("com.niaobulashi.mapper")
  3. public class ShiroApplication {
  4. public static void main(String[] args) {
  5. SpringApplication.run(ShiroApplication.class, args);
  6. }
  7. }

3、简单的表设计

无非就是5张表:用户表、角色表、权限表、用户角色表、角色权限表。

看下面这张图,可以说相当明了了。

具体我就不贴出来了,太占篇幅。。直接贴链接:https://github.com/niaobulashi/spring-boot-learning/blob/master/spring-boot-20-shiro/db/test.sql

4、实体类

User.java

  1. @Data
  2. public class User implements Serializable {
  3. private static final long serialVersionUID = -6056125703075132981L;
  4. private Integer id;
  5. private String account;
  6. private String password;
  7. private String username;
  8. }

Role.java

  1. @Data
  2. public class Role implements Serializable {
  3. private static final long serialVersionUID = -1767327914553823741L;
  4. private Integer id;
  5. private String role;
  6. private String desc;
  7. }

5、mapper层

这里概括一下:简单的用户登录权限的Shiro控制涉及到的数据库操作主要有仨

  • 用户登录名查询用户信息
  • 根据用户查询角色信息
  • 根据角色查询权限信息

UserMapper.java/UserMapper.xml

  1. public interface UserMapper {
  2. /**
  3. * 根据账户查询用户信息
  4. * @param account
  5. * @return
  6. */
  7. User findByAccount(@Param("account") String account);
  8. }
  1. <!--用户表结果集-->
  2. <sql id="base_column_list">
  3. id, account, password, username
  4. </sql>
  5. <!--根据账户查询用户信息-->
  6. <select id="findByAccount" parameterType="Map" resultType="com.niaobulashi.model.User">
  7. select
  8. <include refid="base_column_list"/>
  9. from user
  10. where account = #{account}
  11. </select>

RoleMapper.java/RoleMapper.xml

  1. public interface RoleMapper {
  2. /**
  3. * 根据userId查询角色信息
  4. * @param userId
  5. * @return
  6. */
  7. List<Role> findRoleByUserId(@Param("userId") Integer userId);
  8. }
  1. <!--角色表字段结果集-->
  2. <sql id="base_cloum_list">
  3. id, role, desc
  4. </sql>
  5. <!--根据userId查询角色信息-->
  6. <select id="findRoleByUserId" parameterType="Integer" resultType="com.niaobulashi.model.Role">
  7. select r.id, r.role
  8. from role r
  9. left join user_role ur on ur.role_id = r.id
  10. left join user u on u.id = ur.user_id
  11. where 1=1
  12. and u.user_id = #{userId}
  13. </select>

PermissionMapper.java/PermissionMapper.xml

  1. public interface PermissionMapper {
  2. /**
  3. * 根据角色id查询权限
  4. * @param roleIds
  5. * @return
  6. */
  7. List<String> findByRoleId(@Param("roleIds") List<Integer> roleIds);
  8. }
  1. <!--权限查询结果集-->
  2. <sql id="base_column_list">
  3. id, permission, desc
  4. </sql>
  5. <!--根据角色id查询权限-->
  6. <select id="findByRoleId" parameterType="List" resultType="String">
  7. select permission
  8. from permission, role_permission rp
  9. where rp.permission_id = permission.id and rp.role_id in
  10. <foreach collection="roleIds" item="id" open="(" close=")" separator=",">
  11. #{id}
  12. </foreach>
  13. </select>

6、Service层

没有其他逻辑,只有继承。

注意:

不过需要注意的一点是,我在Service层中,使用的注解@Service:启动时会自动注册到Spring容器中。

否则启动时,拦截器配置初始化时,会找不到Service。。。这点有点坑。

UserService.java/UserServiceImpl.java

  1. public interface UserService {
  2. /**
  3. * 根据账户查询用户信息
  4. * @param account
  5. * @return
  6. */
  7. User findByAccount(String account);
  8. }
  1. @Service("userService")
  2. public class UserServiceImpl implements UserService {
  3. @Resource
  4. private UserMapper userMapper;
  5. /**
  6. * 根据账户查询用户信息
  7. * @param account
  8. * @return
  9. */
  10. @Override
  11. public User findByAccount(String account) {
  12. return userMapper.findByAccount(account);
  13. }
  14. }

RoleService.java/RoleServiceImpl.java

  1. public interface RoleService {
  2. /**
  3. * 根据userId查询角色信息
  4. * @param id
  5. * @return
  6. */
  7. List<Role> findRoleByUserId(Integer id);
  8. }
  1. @Service("roleService")
  2. public class RoleServiceImpl implements RoleService {
  3. @Resource
  4. private RoleMapper roleMapper;
  5. /**
  6. * 根据userId查询角色信息
  7. * @param id
  8. * @return
  9. */
  10. @Override
  11. public List<Role> findRoleByUserId(Integer id) {
  12. return roleMapper.findRoleByUserId(id);
  13. }
  14. }

PermissionService.java/PermissionServiceImpl.java

  1. public interface PermissionService {
  2. /**
  3. * 根据角色id查询权限
  4. * @param roleIds
  5. * @return
  6. */
  7. List<String> findByRoleId(@Param("roleIds") List<Integer> roleIds);
  8. }
  1. @Service("permissionService")
  2. public class PermissionServiceImpl implements PermissionService {
  3. @Resource
  4. private PermissionMapper permissionMapper;
  5. /**
  6. * 根据角色id查询权限
  7. * @param roleIds
  8. * @return
  9. */
  10. @Override
  11. public List<String> findByRoleId(List<Integer> roleIds) {
  12. return permissionMapper.findByRoleId(roleIds);
  13. }
  14. }

7、系统统一返回状态枚举和包装方法

状态字段枚举

StatusEnmus.java

  1. public enum StatusEnums {
  2. SUCCESS(200, "操作成功"),
  3. SYSTEM_ERROR(500, "系统错误"),
  4. ACCOUNT_UNKNOWN(500, "账户不存在"),
  5. ACCOUNT_IS_DISABLED(13, "账号被禁用"),
  6. INCORRECT_CREDENTIALS(500,"用户名或密码错误"),
  7. PARAM_ERROR(400, "参数错误"),
  8. PARAM_REPEAT(400, "参数已存在"),
  9. PERMISSION_ERROR(403, "没有操作权限"),
  10. NOT_LOGIN_IN(15, "账号未登录"),
  11. OTHER(-100, "其他错误");
  12. @Getter
  13. @Setter
  14. private int code;
  15. @Getter
  16. @Setter
  17. private String message;
  18. StatusEnums(int code, String message) {
  19. this.code = code;
  20. this.message = message;
  21. }
  22. }

响应包装方法

ResponseCode.java

  1. @Data
  2. @AllArgsConstructor
  3. public class ResponseCode<T> implements Serializable {
  4. private Integer code;
  5. private String message;
  6. private Object data;
  7. private ResponseCode(StatusEnums responseCode) {
  8. this.code = responseCode.getCode();
  9. this.message = responseCode.getMessage();
  10. }
  11. private ResponseCode(StatusEnums responseCode, T data) {
  12. this.code = responseCode.getCode();
  13. this.message = responseCode.getMessage();
  14. this.data = data;
  15. }
  16. private ResponseCode(Integer code, String message) {
  17. this.code = code;
  18. this.message = message;
  19. }
  20. /**
  21. * 返回成功信息
  22. * @param data 信息内容
  23. * @param <T>
  24. * @return
  25. */
  26. public static<T> ResponseCode success(T data) {
  27. return new ResponseCode<>(StatusEnums.SUCCESS, data);
  28. }
  29. /**
  30. * 返回成功信息
  31. * @return
  32. */
  33. public static ResponseCode success() {
  34. return new ResponseCode(StatusEnums.SUCCESS);
  35. }
  36. /**
  37. * 返回错误信息
  38. * @param statusEnums 响应码
  39. * @return
  40. */
  41. public static ResponseCode error(StatusEnums statusEnums) {
  42. return new ResponseCode(statusEnums);
  43. }
  44. }

8、Shiro配置

ShiroConfig.java

  1. @Configuration
  2. public class ShiroConfig {
  3. /**
  4. * 路径过滤规则
  5. * @return
  6. */
  7. @Bean
  8. public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
  9. ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
  10. shiroFilterFactoryBean.setSecurityManager(securityManager);
  11. // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
  12. shiroFilterFactoryBean.setLoginUrl("/login");
  13. shiroFilterFactoryBean.setSuccessUrl("/");
  14. // 拦截器
  15. Map<String, String> map = new LinkedHashMap<>();
  16. // 配置不会被拦截的链接 顺序判断
  17. map.put("/login", "anon");
  18. // 过滤链定义,从上向下顺序执行,一般将/**放在最为下边
  19. // 进行身份认证后才能访问
  20. // authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问
  21. map.put("/**", "authc");
  22. shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
  23. return shiroFilterFactoryBean;
  24. }
  25. /**
  26. * 自定义身份认证Realm(包含用户名密码校验,权限校验等)
  27. * @return
  28. */
  29. @Bean
  30. public AuthRealm authRealm() {
  31. return new AuthRealm();
  32. }
  33. @Bean
  34. public SecurityManager securityManager() {
  35. DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
  36. securityManager.setRealm(authRealm());
  37. return securityManager;
  38. }
  39. /**
  40. * 开启Shiro注解模式,可以在Controller中的方法上添加注解
  41. * @param securityManager
  42. * @return
  43. */
  44. @Bean
  45. public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
  46. AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
  47. authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
  48. return authorizationAttributeSourceAdvisor;
  49. }
  50. }

扩展:权限拦截Filter的URL的一些说明

这里扩展一下权限拦截Filter的URL的一些说明

1、URL匹配规则

(1)“?”:匹配一个字符,如”/admin?”,将匹配“ /admin1”、“/admin2”,但不匹配“/admin”

(2)“”:匹配零个或多个字符串,如“/admin”,将匹配“ /admin”、“/admin123”,但不匹配“/admin/1”

(3)“”:匹配路径中的零个或多个路径,如“/admin/”,将匹配“/admin/a”、“/admin/a/b”

2、shiro过滤器

Filter 解释
anon 无参,开放权限,可以理解为匿名用户或游客
authc 无参,需要认证
logout 无参,注销,执行后会直接跳转到shiroFilterFactoryBean.setLoginUrl(); 设置的 url
authcBasic 无参,表示 httpBasic 认证
user 无参,表示必须存在用户,当登入操作时不做检查
ssl 无参,表示安全的URL请求,协议为 https
perms[user] 参数可写多个,表示需要某个或某些权限才能通过,多个参数时写 perms["user, admin"],当有多个参数时必须每个参数都通过才算通过
roles[admin] 参数可写多个,表示是某个或某些角色才能通过,多个参数时写 roles["admin,user"],当有多个参数时必须每个参数都通过才算通过
rest[user] 根据请求的方法,相当于 perms[user:method],其中 method 为 post,get,delete 等
port[8081] 当请求的URL端口不是8081时,跳转到schemal://serverName:8081?queryString 其中 schmal 是协议 http 或 https 等等,serverName 是你访问的 Host,8081 是 Port 端口,queryString 是你访问的 URL 里的 ? 后面的参数

常用的主要就是 anon,authc,user,roles,perms 等

注意:anon, authc, authcBasic, user 是第一组认证过滤器,perms, port, rest, roles, ssl 是第二组授权过滤器,要通过授权过滤器,就先要完成登陆认证操作(即先要完成认证才能前去寻找授权) 才能走第二组授权器(例如访问需要 roles 权限的 url,如果还没有登陆的话,会直接跳转到 shiroFilterFactoryBean.setLoginUrl(); 设置的 url )。

9、自定义Realm

主要继承AuthorizingRealm,重写里面的方法doGetAuthorizationInfodoGetAuthenticationInfo

授权:doGetAuthorizationInfo

认证:doGetAuthenticationInfo

AuthRealm.java

  1. public class AuthRealm extends AuthorizingRealm {
  2. @Resource
  3. private UserService userService;
  4. @Resource
  5. private RoleService roleService;
  6. @Resource
  7. private PermissionService permissionService;
  8. /**
  9. * 授权
  10. * @param principalCollection
  11. * @return
  12. */
  13. @Override
  14. protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
  15. User user = (User) principalCollection.getPrimaryPrincipal();
  16. SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
  17. // 根据用户Id查询角色信息
  18. List<Role> roleList = roleService.findRoleByUserId(user.getId());
  19. Set<String> roleSet = new HashSet<>();
  20. List<Integer> roleIds = new ArrayList<>();
  21. for (Role role : roleList) {
  22. roleSet.add(role.getRole());
  23. roleIds.add(role.getId());
  24. }
  25. // 放入角色信息
  26. authorizationInfo.setRoles(roleSet);
  27. // 放入权限信息
  28. List<String> permissionList = permissionService.findByRoleId(roleIds);
  29. authorizationInfo.setStringPermissions(new HashSet<>(permissionList));
  30. return authorizationInfo;
  31. }
  32. /**
  33. * 认证
  34. * @param authToken
  35. * @return
  36. * @throws AuthenticationException
  37. */
  38. @Override
  39. protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authToken) throws AuthenticationException {
  40. UsernamePasswordToken token = (UsernamePasswordToken) authToken;
  41. // 根据用户名查询用户信息
  42. User user = userService.findByAccount(token.getUsername());
  43. if (user == null) {
  44. return null;
  45. }
  46. return new SimpleAuthenticationInfo(user, user.getPassword(), getName());
  47. }
  48. }

10、Contrller层

  1. @RestController
  2. public class LoginController {
  3. /**
  4. * 登录操作
  5. * @param user
  6. * @return
  7. */
  8. @RequestMapping(value = "/login", method = RequestMethod.POST)
  9. public ResponseCode login(@RequestBody User user) {
  10. Subject userSubject = SecurityUtils.getSubject();
  11. UsernamePasswordToken token = new UsernamePasswordToken(user.getAccount(), user.getPassword());
  12. try {
  13. // 登录验证
  14. userSubject.login(token);
  15. return ResponseCode.success();
  16. } catch (UnknownAccountException e) {
  17. return ResponseCode.error(StatusEnums.ACCOUNT_UNKNOWN);
  18. } catch (DisabledAccountException e) {
  19. return ResponseCode.error(StatusEnums.ACCOUNT_IS_DISABLED);
  20. } catch (IncorrectCredentialsException e) {
  21. return ResponseCode.error(StatusEnums.INCORRECT_CREDENTIALS);
  22. } catch (Throwable e) {
  23. e.printStackTrace();
  24. return ResponseCode.error(StatusEnums.SYSTEM_ERROR);
  25. }
  26. }
  27. @GetMapping("/login")
  28. public ResponseCode login() {
  29. return ResponseCode.error(StatusEnums.NOT_LOGIN_IN);
  30. }
  31. @GetMapping("/auth")
  32. public String auth() {
  33. return "已成功登录";
  34. }
  35. @GetMapping("/role")
  36. @RequiresRoles("vip")
  37. public String role() {
  38. return "测试Vip角色";
  39. }
  40. @GetMapping("/permission")
  41. @RequiresPermissions(value = {"add", "update"}, logical = Logical.AND)
  42. public String permission() {
  43. return "测试Add和Update权限";
  44. }
  45. /**
  46. * 登出
  47. * @return
  48. */
  49. @GetMapping("/logout")
  50. public ResponseCode logout() {
  51. getSubject().logout();
  52. return ResponseCode.success();
  53. }
  54. }

四、测试

1、登录:http://localhost:8081/login

  1. {
  2. "account":"123",
  3. "password":"232"
  4. }

2、其他的是get请求,直接发URL就行啦。

已通过接口测试,大家可放心食用。


推荐阅读:

张开涛老的《跟我学Shiro》https://jinnianshilongnian.iteye.com/blog/2018936

Spring Boot2(十二):手摸手教你搭建Shiro安全框架的更多相关文章

  1. Spring Boot2(十五):Shiro记住我rememberMe、验证码Kaptcha

    接着上次学习的<Spring Boot2(十二):手摸手教你搭建Shiro安全框架>,实现了Shiro的认证和授权.今天继续在这个基础上学习Shiro实现功能记住我rememberMe,以 ...

  2. iOS动画进阶 - 手摸手教你写 Slack 的 Loading 动画

    如果移动端访问不佳,可以访问我的个人博客 前几天看了一篇关于动画的博客叫手摸手教你写 Slack 的 Loading 动画,看着挺炫,但是是安卓版的,寻思的着仿造着写一篇iOS版的,下面是我写这个动画 ...

  3. 手摸手教你如何在 Python 编码中做到小细节大优化

    手摸手教你如何在 Python 编码中做到小细节大优化 在列表里计数 """ 在列表里计数,使用 Python 原生函数计数要快很多,所以尽量使用原生函数来计算. &qu ...

  4. 【手摸手,带你搭建前后端分离商城系统】03 整合Spring Security token 实现方案,完成主业务登录

    [手摸手,带你搭建前后端分离商城系统]03 整合Spring Security token 实现方案,完成主业务登录 上节里面,我们已经将基本的前端 VUE + Element UI 整合到了一起.并 ...

  5. 手摸手,带你用Beego撸商城系列二(登录篇)

    完整项目地址: go-shop-b2c 系列文章: 手摸手,带你用 Beego撸商城 系列一(基础篇) 手摸手,带你用 Beego撸商城 系列二(登录篇) 手摸手,带你用 Beego撸商城 系列三(系 ...

  6. 手摸手教你微信小程序开发之自定义组件

    前言 相信大家在开发小程序时会遇到某个功能多次使用的情况,比如弹出框.这个时候大家首先想到的是组件化开发,就是把弹出框封装成一个组件,然后哪里使用哪里就调用,对,看来大家都是有思路的人,但是要怎样实现 ...

  7. 【转】手摸手,带你用vue撸后台 系列二(登录权限篇)

    前言 拖更有点严重,过了半个月才写了第二篇教程.无奈自己是一个业务猿,每天被我司的产品虐的死去活来,之前又病了一下休息了几天,大家见谅. 进入正题,做后台项目区别于做其它的项目,权限验证与安全性是非常 ...

  8. 手摸手教你让Laravel开发Api更得心应手

    https://www.guaosi.com/2019/02/26/laravel-api-initialization-preparation/ 1. 起因 随着前后端完全分离,PHP也基本告别了v ...

  9. 手摸手带你用Hexo撸博客(二)之配置主题

    在上一篇博客手摸手带你用Hexo撸博客(一)中主要介绍了博客的初步搭建 今天我们继续讲如何在Hexo搭建的博客中应用主题 官网选择自己喜欢的主题 点击这里Hexo主题进入官网主题页面 然后选择自己喜欢 ...

随机推荐

  1. 加快QT工程编译速度(还可给Qt for Android设置)

    一.多核编译 环境:win10, Qt 5.4.1,编译器mingw32 项目: Qt for Android Qt Creator 在编译android项目时不支持预编译,默认cpu单核编译,工程稍 ...

  2. Windows7,程序兼容助手:这个程序可能安装不正确(做注册表里设置白名单,软件自身的名字不能带setup)

    Windows上有一个很奇怪的一个现象,一个exe只要名字里面带了setup\install之类的,打开exe后立即退出就会弹出下面的窗口. 解决方法: 方法一.更改exe的名字,去掉setup\in ...

  3. 怎么给开源项目提PR?

    1. fork 你要的项目 2. 下载到本地 相关步骤如下 在你需要的文件夹下面,右键 git bash 命令,打开 git 命令框 执行如下指令可将项目代码下载到当前目录 ~~~ git clone ...

  4. HTTP的请求方法一共有9种,有OPTIONS, HEAD, GET, POST等等(消息头有图,十分清楚)

    请求方法:指定了客户端想对指定的资源/服务器作何种操作 下面我们介绍HTTP/1.1中可用的请求方法: [GET:获取资源]     GET方法用来请求已被URI识别的资源.指定的资源经服务器端解析后 ...

  5. Android零基础入门第11节:简单几步带你飞,运行Android Studio工程

    原文:Android零基础入门第11节:简单几步带你飞,运行Android Studio工程 之前讲过Eclipse环境下的Android虚拟设备的创建和使用,现在既然升级了Android Studi ...

  6. .NET DataTable转换为JSON格式的字符串

    在进行数据传递的时候,有时我们需要通过Ajax的方式或者其他的方式传递一个数据列表,可以将DataTable或者其他形式的数据列表转换为JSON的格式,通过Ajax实体的形式进行传递. 比如说: // ...

  7. wchar_t string on Linux, OS X and Windows

    Making wchar_t work on Linux, OS X and Windows for CMarkup release 10.1 I learned a couple of humble ...

  8. MySQL 主从配置 读写分离

    Master配置 1.创建用户: 在Master MySQL上创建一个用户‘repl’,并允许其他Slave服务器可以通过远程访问Master,通过该用户读取二进制日志,实现数据同步. create ...

  9. 初涉Delphi Socket编程

    不是第一次接触socket编程了,但以前都是看别人的依葫芦画瓢,也不知道具体的原理. 新的项目,有了新的开始,同时也需要有新的认识. Delphi 中带有两套TCP Socket组件: Indy So ...

  10. 沙漏集合 good

    曾经的高考状元,如今都过得怎么样呢?http://www.toutiao.com/a6428794132465975554/ 你可知道,古代女人为什么不能当官——笑昏我了http://tieba.ba ...