基于Springboot的SSO单点登陆系统的登陆操作实战

2023-10-30

一 前言

(1)使用环境:

SpringBoot2.X

MyBatis

基于redis存储的springSession

(2)基础学习:

关于SSO的基础学习可以参考该文章:单点登录(SSO),从原理到实现

代码风格使用的是晓风轻的代码规范,对于其中的AOP实现此处不会给出代码,具体可以在文章尾部的gitHub上查看:我的编码习惯 - Controller规范

进阶可以参考:单点登录(一)-----理论-----单点登录SSO的介绍和CAS+选型

(3)目标

  1. 使用Header认证替换Cookie,避免用户禁用cookie导致登陆失效的情况
  2. 实现可以运行操作的SSO单点登录系统

(4)注意:

  1. 此处使用了一个项目来模拟一个Client与一个Server,因为Server依靠存储token来判断用户是否登陆,而Client依靠Session判断用户是否登陆,因此两者能在同个项目共存。
  2. 由于项目的依赖很多,所以不会事无巨细地讲,只会挑重点的看,具体的可以在文章尾部的GitHub上查看

看完以上文章之后总结一下,在这次简单实现中我们需要做到的有以下几点:

  1. Client服务端收到请求,Filter拦截该请求,在Filter中判断该用户是否已经登陆,如果已经登陆,就直接进入系统,否则,返回用户没有登陆的信息,由前端页面进行跳转到SSO服务器的登录页面,此时要带上原页面的url,下面成为clientUrl。
  2. 在LoginURL中会获取到用户的token,检验用户是否已经在其他相关使用SSO的系统登陆成功。如果已经在其他的系统登陆了,则将请求转回Client,并且带回一个token, Client再次发送请求到ValidateURL。否则,系统提示用户输入ID和PASSWORD。
  3. 提交后请求到ValidateURL,Server验证token的有效性。然后返回结果给Client。如果token有效,则Client与用户之间建立局部会话。否则,重定向到登陆页面,提示用户输入ID和PASSWORD。
  4. 校验ID和Password是否匹配。如不匹配,再次要求用户输入ID和PASSWORD。否则,Server记录用户登陆成功。并向浏览器回送token,记录用户已经登陆成功。
    那么马上开始SSO单点登陆之旅

二 基础工具类

首先来看下基础工具类,比较多,不想看的朋友可以跳到下一大节,下面看到有不清楚的方法可以搜索回来这里看。以下在gitHub上都有:

配置redis:

@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
    @Bean
    <T> RedisTemplate<String, T> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, T> redisTemplate = new RedisTemplate<>();
        System.out.println("加载redis配置");

        Jackson2JsonRedisSerializer j = new Jackson2JsonRedisSerializer(Object.class);
        // value值得序列化采用fastJsonRedisSerializer
        redisTemplate.setValueSerializer(j);
        redisTemplate.setHashValueSerializer(j);
        // key的序列化采用StringRedisSerializer
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setConnectionFactory(factory);
        redisTemplate.setEnableTransactionSupport(true);
        return redisTemplate;
    }
}

使用SpringSession替代Tomcat内置的Session,并且设置为Header认证:

@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1801)
public class HttpSessionConfig {
    @Bean
    public HeaderHttpSessionIdResolver httpSessionStrategy() {
        return new HeaderHttpSessionIdResolver("x-auth-token");
    }
}

此处是一个HttpClient类,使用其架起两个服务器之间的通信:

@Service("httpClientUtil")
@Slf4j
public class HttpClientUtil {

    /**
     * 使用Json格式发送Post请求
     *
     * @param url         发送的URL
     * @param requestData 传给数据
     * @return
     * @throws IOException
     */
    public ResultBean<Data> postAction(String url, RequestBean requestData) throws IOException {
        // 将Json对象转换为字符串
        Gson gson = new Gson();
        String strJson = gson.toJson(requestData, requestData.getClass());
        log.info("httpClient发送数据:{}", strJson);
        //使用帮助类HttpClients创建CloseableHttpClient对象.
        CloseableHttpClient client = HttpClients.createDefault();
        //HTTP请求类型创建HttpPost实例
        HttpPost httpPost = new HttpPost(url);

        httpPost.setHeader("Content-Type", "application/json;charset=UTF-8");

        //组织数据
        StringEntity se = new StringEntity(strJson);
        se.setContentType("application/json");

        //对于httpPost请求,把请求体填充进HttpPost实体.
        httpPost.setEntity(se);

        CloseableHttpResponse response = null;
        try {
            // 执行请求
            response = client.execute(httpPost);
            HttpEntity entity = response.getEntity();
            // 判断返回状态是否为200
            if (response.getStatusLine().getStatusCode() == 200) {
                strJson = EntityUtils.toString(entity, "UTF-8").trim();
                Type type = new TypeToken<ResultBean<Data>>() {
                }.getType();
                ResultBean<Data> resultBean = VerifyUtil.cast(gson.fromJson(strJson, type));

                // 处理子系统局部会话的Header认证
                if (null != response.getFirstHeader("x-auth-token")) {
                    if (null == resultBean.getData()) {
                        resultBean.setData(new Data());
                    }
                    resultBean.getData().setAuthToken((response.getFirstHeader("x-auth-token").toString()).split(" ")[1]);
                }
                return resultBean;
            }
            return null;
        } finally {
            if (response != null) {
                response.close();
            }
            client.close();
        }
    }
}

一个会经常用到的工具类:

public class VerifyUtil implements Serializable {
	/**
     * 字符串判空
     * @param str 字符串
     * @return 不为空返回true
     */
    public static boolean isNotEmpty(String str) {
        return (null != str && !str.equals(""));
    }

    /**
     * 字符串判空
     * @param args 多个字符串
     * @return 全部不为空返回true
     */
    public static boolean isNotEmpty(String... args) {
        for (String str : args) {
            if (null == str || str.equals("")) {
                return false;
            }
        }
        return true;
    }

    /**
     * 检查字符串是否为null
     * @param arg 多个字符串
     * @return 全部不为null返回true
     */
    public static boolean checkNull(String...arg) {
        for (String str : arg) {
            if (!checkNull(str)) {
                return false;
            }
        }
        return true;
    }

    /**
     * 对象判空
     * @param object 对象
     * @return 不为空返回true
     */
    public static boolean checkNull(Object object) {
        return null != object;
    }

    @SuppressWarnings("unchecked")
    public static <T> T cast(Object object) {
        return (T)object;
    }
}

一个用户状态的枚举类:

@AllArgsConstructor
public enum UserStatusEnum {

    PARAMETER_ERROR("parameter_error"),

    USER_ACCOUNT_ERROR("user_account_error"),

    USER_HAS_REGISTER("user_has_register"),

    URL_OR_TOKEN_ERROR("url or token error"),

    USER_HAS_NOT_LOGIN("you has not login")
    ;

    private String msg;

    public String getMsg() {
        return msg;
    }
}

系统之间进行交互的实体类:

@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class RequestBean {
    User user;

    String token;

    String clientUrl;

    String authToken;
}
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ResultBean<T> implements Serializable {

    private static final long serialVersionUID = 1L;

    public static final int NO_LOGIN = -1;

    public static final int SUCCESS = 0;

    public static final int FAIL = 1;

    public static final int NO_PERMISSION = 2;

    private String msg = "success";

    private int code = SUCCESS;

    private T data;

    public ResultBean() {
        super();
    }

    public ResultBean(T data) {
        super();
        this.data = data;
    }

    public ResultBean(Throwable e) {
        super();
        this.msg = e.toString();
        this.code = FAIL;
    }
}
@lombok.Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class Data {
    User user;

    /**
     * 单点登录令牌
     */
    String token;

    String clientUrl;

    String authToken;
}

三 正文

那么现在就进入正文了,整个项目还是采用了MVC的架构,下面我们将主要看下controller层与service层:

首先是controller层:

@CrossOrigin
@RestController
@RequestMapping("/user")
public class UserController {

    @Resource
    UserService userService;

    // sso服务端
    /**
     * 用户登陆
     * @param requestBean user、clientUrl、token
     * @return token、clientUrl、authToken
     */
    @PostMapping("/login")
    public ResultBean<Data> login(@RequestBody RequestBean requestBean) {
        return new ResultBean<>(userService.login(requestBean));
    }

    /**
     * 验证客户端的token与clientUrl是否合法,若合法则将客户端的clientUrl注册到token中
     * @param requestBean token、clientUrl
     * @return 操作结果,成功data为null
     */
    @PostMapping("/valid")
    public ResultBean<Data> validToken(@RequestBody RequestBean requestBean) {
        return new ResultBean<>(userService.valid(requestBean));
    }

    // sso客户端
    
    /**
     * 接收来自服务器的token与clientUrl,
     * @param httpSession 操作session
     * @param requestBean token、clientUrl
     * @return 操作结果,成功data为带token与clientUrl
     */
    @PostMapping("/token")
    public ResultBean<Data> token(HttpSession httpSession, @RequestBody RequestBean requestBean) {
        ResultBean<Data> resultBean = new ResultBean<>(userService.token(requestBean));
        // 验证成功
        if (resultBean.getCode() == 0) {
//             此处仅仅设置用户会话,用户信息的获取在其他请求处理
            System.out.println("设置session");
            httpSession.setAttribute("user", new User());
        }
        return resultBean;
    }
}

一共只有三个方法,下面说下三者各自的任务:

在login()中对用户的登录状态进行判断,验证用户的token,若用户仍未登录则提示用户登录。若用户处于登录状态(无论是以注册token还是刚使用ID与PASSWORD登录),则利用请求中的clientUrl + "/user/token"将token传输给Client(子服务器)。注意,在login()中并没有将clientUrl注册进token内,clientUrl需要经过Client验证后方才可以注册进token。

在token()中则是Client接收SSO Server传输过来的token数据,并且将其发送给serverUrl + "/user/valid"进行验证,若验证通过,则为用户设置user的Session属性,并且将该session对应的x-auth-token传递回去给SSO Server。

valid()中验证并将clientUrl注册进token中。

下面让我们来继续深入service层:

public interface UserService {
    /**
     * 用户登陆
     * @param requestBean user、clientUrl、token
     * @return token、clientUrl、authToken
     */
    Data login(RequestBean requestBean);

    /**
     * 验证客户端的token与clientUrl是否合法,若合法则将客户端的clientUrl注册到token中
     * @param requestBean token、clientUrl
     * @return 操作结果,成功data为null
     */
    Data valid(RequestBean requestBean);

    /**
     * 接收来自服务器的token与clientUrl,
     * @param requestBean token、clientUrl
     * @return 操作结果,成功data为带token与clientUrl
     */
    Data token(RequestBean requestBean);
}

具体比较重要的实现类,这里我将按照前面所说的SSO流程来进行说明,即是按照一个请求从头走到尾的方式。同样,需要完整代码的看文末的github:

首先是基础内容:

@Service
@Slf4j
public class UserServiceImpl implements UserService {

    /**
     * 用户的Mysql数据库操作
     */
    @Resource
    UserDao userDao;

    /**
     * 服务器之间的通讯方式
     */
    @Resource
    HttpClientUtil httpClientUtil;

    /**
     * redis数据库操作
     */
    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 建立token与clientUrl的映射,系统注销的时候会用到
     */
    private static Map<String, List<String>> tokenAndUrlMap = new HashMap<>();

    /**
     * 建立token与user信息的映射,子系统请求用户信息的时候回用到
     */
    private static Map<String, User> tokenAndUserMap = new HashMap<>();

    /**
     * 建立token与sessionID,即x-auth-token之间的映射,用户请求x-auth-token的时候会用到
     */
    private static Map<String, String> tokenAndSessionId = new HashMap<>();
}

首先从登陆开始:

/**
 * 验证用户是否已登录、验证请求参数是否合法
 * @param requestBean user、clientUrl、token
 * @return token、clientUrl、authToken
 */
@Override
public Data login(RequestBean requestBean) {
    String token = requestBean.getToken();
    String clientUrl = requestBean.getClientUrl();
    log.info("login() : token = {}, clientUrl = {}", token, clientUrl);

    // 用户使用token、clientUrl代表需要查询是否已登录
    // tokenAndUrlMap中包含key代表已登录
    if (isNotEmpty(token, clientUrl) && tokenAndUrlMap.containsKey(token)) {
        log.info("token = {} 用户已登陆", token);
        // 将信息反馈给Client
        Data data = transmitToken(token, clientUrl);
        tokenAndSessionId.put(token, data.getAuthToken());
        return data;
    }

    // 使用账号密码进行登陆

    User user = requestBean.getUser();

    // 登陆信息为空,说明用户意图不为登陆,抛出未登录状态的异常
    if (!checkNull(user)) {
        throw new UnloginException(UserStatusEnum.USER_HAS_NOT_LOGIN.getMsg());
    }

    // 检验合法参数,并进入登陆逻辑
    if (isNotEmpty(user.getAccount(), user.getPassword(), clientUrl)) {
        return loginImpl(user, clientUrl);
    }

    // 说明参数检验不通过,抛出参数错误异常
    throw new CheckException(UserStatusEnum.PARAMETER_ERROR.getMsg());
}

说下我这里的实现都是使用X()先对参数进行一些基本的验证,验证不通过则直接抛出异常,否则进入XImpl()进行逻辑处理。

	/**
     * 用户登录逻辑实现
     * @param user 用户信息:account、password
     * @param clientUrl Client的url
     * @return token、clientUrl、authToken
     */
    private Data loginImpl(User user, String clientUrl) {
        String account = user.getAccount();
        String password = user.getPassword();

        // 查询数据库,若无此用户数据则抛出账号错误异常
        user = userDao.listUserByUAccountAndPassword(account, password);
        if (!checkNull(user)) {
            throw new CheckException(UserStatusEnum.USER_ACCOUNT_ERROR.getMsg());
        }

        // 使用uuid生成token并存入tokenMap中,注意此时并没有注册clientUrl
        String token = UUID.randomUUID().toString();
        tokenAndUrlMap.put(token, new ArrayList<>());
        tokenAndUserMap.put(token, user);

        // 将信息反馈给Client、
        Data data = transmitToken(token, clientUrl);
        tokenAndSessionId.put(token, data.getAuthToken());

        log.info("loginImpl() 登录成功:token = {}, clientUrl = {}, sesssionId = {}, User = {}", token, clientUrl, tokenAndSessionId.get(token), user);
        return data;
    }

此处给出login()的数据格式:

{
	"user":{
		"account":"1",
		"password":"1"
	},
	"clientUrl":"http://localhost:8889",
	"token":"b8b51c17-dcb8-414f-af62-e2d1f3f49037"
}

操作流程:

  1. 当用户携带token与clientUrl时,需要查询相应的token是否已登录,若已登录则将信息反馈给子系统。
  2. 当用户没有携带token或者token未登录时,开始检验用户登录的账号密码参数,检验成功则查询数据库是否存在符合条件的用户,若符合条件则将信息反馈给子系统

不过大家应该发现了transmitToken(token, clientUrl)这个方法,可以猜测到他实现的功能应该是利用clientUrl将token传递给子系统,并且带回authToken,即x-auth-token。相当于用户在子系统中局部会话的JESSIONID。

	/**
     * 用以将生成的token或者以验证登陆的token传递回去给Client
     *
     * @param token     令牌
     * @param clientUrl Client的url
     */
    private Data transmitToken(String token, String clientUrl) {
        try {
            return (httpClientUtil.postAction(clientUrl + "/user/token", new RequestBean().setToken(token).setClientUrl(clientUrl)).getData());
        } catch (IOException e) {
            e.printStackTrace();
            throw new ErrorException("clientUrl error");
        }
    }

这里的方法参考上一大节的HttpClient工具类,操作是把RequestBean当做参数,发送请求到clientUrl + "/user/token"中,并且返回类型为Data的参数。从clientUrl + "/user/token"开始大家应该想到这个是服务器与服务器之间的请求了。那么从controller层进入:

	/**
     * 接收来自服务器的token与clientUrl,
     * @param httpSession 操作session
     * @param requestBean token、clientUrl
     * @return 操作结果,成功data为带token与clientUrl
     */
    // sso客户端
    @PostMapping("/token")
    public ResultBean<Data> token(HttpSession httpSession, @RequestBody RequestBean requestBean) {
        ResultBean<Data> resultBean = new ResultBean<>(userService.token(requestBean));
        // 验证成功
        if (resultBean.getCode() == 0) {
//             此处仅仅设置用户会话,用户信息的获取在其他请求处理
            System.out.println("设置session");
            httpSession.setAttribute("user", new User());
        }
        return resultBean;
    }

从SSO的逻辑中,我们知道当SSO认证中心把token发回给子系统时,子系统需要使用token在SSO认证中心进行注册,在验证注册成功后,设置局部会话,并且需要把设置局部会话(即Session)产生的x-auth-token传递回给SSO认证中心。

那么我们继续看子系统的token方法应该怎样处理此处的逻辑的:

/**
 * 验证来自服务器的token与clientUrl参数合法性,
 * @param requestBean token、clientUrl
 * @return 操作结果,成功data为带token与clientUrl
 */
@Override
public Data token(RequestBean requestBean) {
    String token = requestBean.getToken();
    String clientUrl = requestBean.getClientUrl();
    if (isNotEmpty(token, clientUrl)) {
        return tokenImpl(token, clientUrl);
    }
    throw new CheckException(UserStatusEnum.PARAMETER_ERROR.getMsg());
}

验证参数合法性,合法则进入逻辑处理:

	private Data tokenImpl(String token, String clientUrl) {
        if (remoteValid(token, clientUrl)) {
            return new Data().setToken(token).setClientUrl(clientUrl);
        }
        throw new ErrorException("valid error");
    }
	/**
     * 向SSO发送令牌与本地url,验证注册
     *
     * @param token     令牌
     * @param clientUrl 子系统url
     * @return true 验证成功
     */
    private boolean remoteValid(String token, String clientUrl) {
        try {
            // 0为验证成功
            if ((httpClientUtil.postAction("http://localhost:8889/user/valid", new RequestBean().setToken(token).setClientUrl(clientUrl))).getCode() == 0) {
                return true;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }

这里是将SSO认证中心发来的token发回SSO认证中心进行认证注册,我们在上面用户登录的时候并没有对用户的clientUrl进行注册,注册行为在此处才会发生。

这里我们再次跳转到SSO认证中心的valid方法,controller层没有特殊处理,直接看service层:

	/**
     * 检验基本的token、clientUrl参数的合法性
     * @param requestBean token、clientUrl
     * @return 操作结果,成功data为null
     */
    @Override
    public Data valid(RequestBean requestBean) {
        String token = requestBean.getToken();
        String clientUrl = requestBean.getClientUrl();
        if (isNotEmpty(token, clientUrl)) {
            return validImpl(token, clientUrl);
        }

        throw new CheckException(UserStatusEnum.PARAMETER_ERROR.getMsg());
    }
	/**
     * 验证客户端的token与clientUrl是否合法,若合法则将客户端的clientUrl注册到token中
     * @param token  令牌,用户SSO认证中心登录的凭证
     * @param clientUrl 子系统的url
     * @return 操作结果,成功data为null
     */
    private Data validImpl(String token, String clientUrl) {
        boolean hasSave = false;

        // 验证数据是否合法且token是否存在
        if (tokenAndUrlMap.containsKey(token)) {
            List<String> urls = tokenAndUrlMap.get(token);
            // 验证url是否已保存
            if (null != urls) {
                for (String url : urls) {
                    if (url.contains(clientUrl)) {
                        hasSave = true;
                    }
                }
            }

            if (!hasSave) {
                urls.add(clientUrl);
            }
            // 返回null即可以,默认成功
            return null;
        } else {
            throw new ErrorException("has not exist token");
        }
    }

可以看到的是我们在这里才开始进行了token与clientUrl的注册,之所以要加进去是为了以后注销功能的实现。

此处的功能比较简单,对token进行验证,验证通过之后就将其加入到tokenAndurlMap的映射中。

到这里就是登录的逻辑,最后SSO认证中心需要把用户的token以及用户在子系统的局部会话的x-auth-token返回给用户。

其Json格式为:

{
    "msg": "success",
    "code": 0,
    "data": {
        "user": null,
        "token": "d82ea315-1034-48d2-8e0d-5303c65d4e6a",
        "clientUrl": "http://localhost:8889",
        "authToken": "56e20f4e-f473-4882-8b11-24aa006d1ab3"
    }
}

四 总结

总的来说登录逻辑分为以下几步:

  1. 用户在SSO认证中心进行登录,SSO认证中心将随机生成的token传递给子服务器
  2. 子服务器接收到数据后,对SSO认证中心发起验证请求。
  3. SSO认证中心接收到验证请求后判断token是否合法,若合法则将其加入映射。并将操作结果返回
  4. 子服务器新建Session局部会话,并将x-auth-token返回给SSO认证中心
  5. SSO认证中心将token、x-auth-token、clientUrl返回给用户。用户可根据token进行多系统登录,利用x-auth-token得到与某一子系统的局部会话。

本次的SSO单点登录先写到这里,后面可能会写一篇关于SSO单点登录的注销实现。

项目GitHub地址:https://github.com/attendent/distrubuted

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

基于Springboot的SSO单点登陆系统的登陆操作实战 的相关文章

  • Web项目如何做单元测试

    你可能会用单元测试框架 python的unittest pytest Java的Junit testNG等 那么你会做单元测试么 当然了 这有什么难的 test demo py def inc x return x 1 def test a

随机推荐

  • 在WebStorm中使用Vue的v-bind,v-on等内置指令时报命名空间的错误

    报错详情 Namespace v bind is not bound Namespace v on is not bound 等 问题说明 出现这个错误不是代码本身的问题 而是 WebStorm 这个编辑器的问题 因为 WebStorm 不
  • B+ Tree

    B Tree 什么是B B 树的时间复杂度和高度 Insert 简单的insert 复杂的Insert Delete 简单的delete 复杂的delete 时间复杂度 什么是B B tree是平衡二叉树 每个节点包含k个元素 k的范围在
  • SmartBuffer-让你不再为共享与私有缓存处理逻辑费神

    一 带着问题 找思路 有这样一个需求 有3个算法类 我们分别称为TestAlgo1 TestAlgo2 TestAlgo3 每个算法可以设置使用buffer类型是共享或私有 补充 共享是指在同类型算法的多个实例中使用同一块buffer 比如
  • Python-mock

    文章目录 一 接口测试中Mock的用处 二 使用步骤 1 Moco框架搭建Mock服务 Moco说明 2 Python unittest模块自带的mock 一 接口测试中Mock的用处 前后端开发 后端接口未开发完成 前端调用mock数据进
  • 网易云音乐评论抓取(js逆向)

    网易云音乐评论抓取 js逆向 本文通过分析网易云音乐的js加密 通过构造相关的参数 获取网易云音乐评论 所用语言和相关模块 python3 6 requests 网站特点分析 通过分析网站可知 评论获取的url https music 16
  • JavaScript和C++的一些显著差异和学习tips

    之前学习JS来做Vue Node js前后端 最近开始重新学习C 显然这两者的差异巨大 主要注意的点 C 的语法结构 while for JS中没有数据类型 而C 的数据类型需要学习 例如DWORD等等 面向对象 JavaScript的对象
  • 人工智能基础篇

    人工智能基础篇 本篇目录 一 人工智能 机器学习 深度学习的关系 1 关系图 2 人工智能 3 人类智能过程 4 机器学习 5 深度学习 二 人工智能研究的领域 三 人工智能的应用场景 1 计算机视觉 2 语音技术 3 自然语言处理 4 决
  • C++学习第二弹之整数数据类型

    本文主要介绍C 数 整数 包括创建变量和编写各种类型的常量 整数 不同C 数据类型使用不同的内存来存储整数 内存越大能够表示的数值就越大 同时有的类型可以表示正值和负值 有些不能表示负值 C 基本整型有char short int long
  • C++设计模式由浅入深(一)—— 继承和多态

    一 继承和多态 C 首先是一个面向对象的语言 对象是C 程序的基石 通过类的继承和派生 软件工程师可以自由表达对软件系统中各个部分之间的关系与交互逻辑 定义各个组件之间的接口和实现 有秩序地组织起数据结构和代码 本书的目的不是为了教授C 语
  • 项目——电子词典(客户端、服务器交互,字典导入,单词查询)

    一 项目要求 登录注册功能 不能重复登录 重复注册 单词查询功能 历史记录功能 存储单词 意思 以及查询时间 基于TCP 支持多客户端连接 采用数据库保存用户信息与历史记录 将dict txt的数据导入到数据库中保存 按下ctrl c退出客
  • Fiddler抓包工具总结

    序章 Fiddler是一个蛮好用的抓包工具 可以将网络传输发送与接受的数据包进行截获 重发 编辑 转存等操作 也可以用来检测网络安全 反正好处多多 举之不尽呀 当年学习的时候也蛮费劲 一些蛮实用隐藏的小功能用了之后就忘记了 每次去网站上找也
  • qt 智能指针介绍

    简介 Qt 提供了很多智能指针 比较常见的有 QPointer QSharedDataPointer QSharedPointer QWeakPointer 和 QScopedPointer 描述 QPointer 4 0 已经过时 可以被
  • php fastcgi,配置apache以fastcgi运行php

    apache默认是用自带的mod php模块运行php 现在我们介绍使用fastcgi来执行php脚本 先说下fastcgi的优点 Fastcgi的优点 从稳定性上看 fastcgi是以独立的进程池运行来cgi 单独一个进程死掉 系统可以很
  • 解决高德地图AMap is not defined

    传送门 https blog csdn net qq 36317441 article details 77187369 总结一下 就是将script 标签引入的高德地图地址 放到body中
  • Leetcode 面试题 01.06.字符串压缩(Compress String LCCI)

    Leetcode 面试题 01 06 字符串压缩 1 题目描述 Leetcode题目链接 字符串压缩 利用字符重复出现的次数 编写一种方法 实现基本的字符串压缩功能 比如 字符串aabcccccaaa会变为a2b1c5a3 若 压缩 后的字
  • 虚拟机占用磁盘越来越大的解决方案大总结

    原文链接 https blog csdn net Shine Su article details 119676646 作者 填坑小霸王 在电脑上安装Vmware 安装ubuntu16 04 进行开发 使用过程中没有在意 忽然有一天发现磁盘
  • 【esp32&lvgl】-2.1 # esp32移植lvgl7驱动st7789屏幕(ESP-IDF框架)

    目录 一 前言 二 代码 三 硬件说明 四 ESP IDF设置 重要 五 屏幕偏移修改驱动文件 参考资料 一 前言 前几天在ardiuno框架下基于platformIO用TFT eSPI库实现了lvgl库的移植 这种方法虽然在lvgl的版本
  • 电源系列1:LDO 基本 原理(一)

    微信公众号 工程师看海 后台回复 LDO仿真文件 转载请注明原文地址 https blog csdn net u013608300 article details 108994763 1 前言 目前市场上无论什么电子产品 只要涉及到电就必须
  • ssh 运行 java,通过ssh将Java应用程序作为后台进程运行

    I m currently developing a simple deployment script for vms running ubuntu All these machines are supposed to run a java
  • 基于Springboot的SSO单点登陆系统的登陆操作实战

    一 前言 1 使用环境 SpringBoot2 X MyBatis 基于redis存储的springSession 2 基础学习 关于SSO的基础学习可以参考该文章 单点登录 SSO 从原理到实现 代码风格使用的是晓风轻的代码规范 对于其中