【任务6】注册与登录:初步完善注册功能

请打卡如下内容
1、大致说一下你是怎么限制用户昵称重复的
2、给出你敏感词过滤的算法
3、给出你限制密码强度/长度/字符的算法

对于代码的提交,你可以用

这样就可以高亮了

本问题对应的项目链接:从0设计一个web项目

PS:每一个打卡,都要给出你的GitHub链接哈,并且代码提交到GitHub

共以下 11 个回答

  • 星河揽我入梦 永久会员 2024年6月12日 下午8:48

    github仓库:https://github.com/CurryisCoat/community/tree/dev
    1.在数据库设计表时给用户名字段添加唯一索引;
    2:前缀树

    public class SensitiveFilter {
    
    <pre><code>private static final Logger logger = LoggerFactory.getLogger(SensitiveFilter.class);
    
    private static final String REPLACEMENT = "***";
    
    // 根节点
    private TrieNode rootNode = new TrieNode();
    
    @PostConstruct
    public void init()
    {
        InputStream is = this.getClass().getClassLoader().getResourceAsStream("sensitive-filter.txt");
        BufferedReader read = new BufferedReader(new InputStreamReader(is));
    
        try {
            String keyWord;
            while((keyWord=read.readLine())!=null)
            {
                this.addKeyword(keyWord);
            }
        } catch (IOException e) {
            logger.error("加载敏感词文件失败: " + e.getMessage());
        }
    }
    
    //将一个敏感词添加到前缀树中
    private void addKeyword(String keyWord)
    {
    
        TrieNode tempNode = rootNode;
        for(int i = 0;i < keyWord.length();i ++)
        {
            char c = keyWord.charAt(i);
            TrieNode subNode = tempNode.getSubNode(c);
    
            if(subNode==null){
                subNode = new TrieNode();
                tempNode.addSubNode(c,subNode);
            }
    
            tempNode=subNode;
            if(i == keyWord.length()-1){
                tempNode.setKeywordEnd(true);
            }
        }
    }
    //过滤文本
    public String filter(String text)
    {
        if(StringUtils.isBlank(text)){
            return null;
        }
    
        TrieNode p1 = rootNode;
        int p2 = 0;
        int p3 = 0;
        StringBuilder sb = new StringBuilder();
        while(p3 < text.length())
        {
            char c = text.charAt(p3);
            if(isSymbol(c)){
    
                if(p1 == rootNode){
                    sb.append(c);
                    p2++;
                }
                p3++;
                continue;
            }
    
            p1 = p1.getSubNode(c);
            if(p1==null){
                sb.append(text.charAt(p2));
                p3 = ++p2;
                p1 = rootNode;
            }else if(p1.isKeywordEnd()==true){
                sb.append(REPLACEMENT);
                p2 = ++p3;
                p1 = rootNode;
            }else{
                p3++;
            }
        }
        sb.append(text.substring(p2));
        return sb.toString();
    }
    
    
    // 判断是否为符号
    private boolean isSymbol(Character c) {
        // 0x2E80~0x9FFF 是东亚文字范围
        return !CharUtils.isAsciiAlphanumeric(c) && (c < 0x2E80 || c > 0x9FFF);
    }
    //树结构
    private class TrieNode{
    
        private boolean iskeywordEnd = false;
    
        private HashMap<Character,TrieNode> subNodes = new HashMap<>();
    
        public boolean isKeywordEnd() {
            return iskeywordEnd;
        }
    
        public void setKeywordEnd(boolean keywordEnd) {
            iskeywordEnd = keywordEnd;
        }
    
        public void addSubNode(Character c,TrieNode node){
            subNodes.put(c,node);
        }
    
        public TrieNode getSubNode(Character c){
            return subNodes.get(c);
        }
    }
    
    }
    
    0 赞同 0 条回复
  • 忆霁昕 永久会员 2022年10月25日 上午11:17

    github仓库:https://github.com/yijixin66/FromZerotoExpert
    服务器:http://8.142.128.17/FromZerotoExpert/register.html
    1、 在添加用户名之前查询数据库是否已存在该用户名
    2、 AC自动机,在trie树的基础上加入fail指针,类似于kmp中的next数组,可以在匹配失败时跳过已匹配的部分

    public class AcTrie {
        class AcNode {
            public char data;
            //key-子节点的data value-子节点
            public HashMap children = new HashMap();
            public boolean end = false;
            public AcNode fail;
    
            public AcNode(char data) {
                this.data = data;
            }
        }
    
        private AcNode root = new AcNode('/');
    
        //插入一个字符串
        public void insert(String text) {
            AcNode p = root;
            for (int i = 0; i < text.length(); i++) {
                char key = text.charAt(i);
                if (!p.children.containsKey(key)) {
                    AcNode node = new AcNode(key);
                    p.children.put(key, node);
                }
                p = p.children.get(key);
            }
            p.end = true;
        }
    
        //根据字符串数组构建ac自动机
        public void build(String[] texts) {
            for (String text : texts) {
                insert(text);
            }
            buildFail();
        }
        public void buildFail() {
            //层序遍历
            Queue queue = new LinkedList<>();
            root.fail = null;
            queue.add(root);
            while (!queue.isEmpty()) {
                AcNode p = queue.remove();
                for (AcNode pc : p.children.values()) {
                    if (p == root) {
                        pc.fail = root;
                    } else {
                        AcNode q = p.fail;
                        while (q != null) {
                            AcNode qc = q.children.get(pc.data);
                            if (qc != null) {
                                pc.fail = qc;
                                break;
                            }
                            q = q.fail;
                        }
                        if (q == null) {
                            pc.fail = root;
                        }
                    }
                    queue.add(pc);
                }
            }
        }
        //查找该字符的子串是否存在
        public boolean find(String text) {
            AcNode p = root;
            int i = 0;
            int j = 0;
            while (j < text.length()) {
                char key = text.charAt(j);
                if (p.children.containsKey(key)) {
                    j++;
                    p = p.children.get(key);
                    if (p.end) {
                        return true;
                    }
                } else {
                    //没有匹配上 回退
                    i++;
                    j = i;
                    p = root;
                }
    
            }
            return false;
        }
        public boolean match (String text) {
            AcNode p = root;
            for (int i = 0; i < text.length(); i++) {
                char key = text.charAt(i);
                while (!p.children.containsKey(key) && p != root) {
                    p = p.fail;
                }
                p = p.children.get(key);
                if (p == null) {
                    p = root;
                }
                AcNode tmp = p;
                while (tmp != null) {
                    if (tmp.end) {
                        return true;
                    }
                    tmp = tmp.fail;
                }
            }
            return false;
        }
    

    3、 遍历字符串 在CheckUtils中实现

    0 赞同 0 条回复
  • 豆子彩虹 普通 2022年10月22日 上午11:42
    1. 数据库设置不能重复,添加之前先查询
    2. 前缀树
    @Component
    public class Trie {
        private final TrieNode root;
        public Trie() {
            root = new TrieNode();
        }
    
        /**
         * 向前缀树添加敏感词
         * @param s 敏感词
         */
        public void put(String s) {
            char[] chars = s.toCharArray();
            TrieNode pointer = root;
            for (char aChar : chars) {
                HashMap<Character, TrieNode> map = pointer.getMap();
                if (map.containsKey(aChar)) {
                    pointer = map.get(aChar);
                }else {
                    TrieNode trieNode = new TrieNode(aChar);
                    map.put(aChar, trieNode);
                    pointer = trieNode;
                }
            }
        }
    
        /**
         * 判断语句是否包含敏感词
         * @param s 需要判断的语句
         */
        public boolean ifContain(String s) {
            TrieNode pointer = root;
            for (int i = 0; i < s.length(); i++) {
                int j = i;
                while (j < s.length() && pointer.getMap().containsKey(s.charAt(j)) ) {
                    pointer = pointer.getMap().get(s.charAt(j));
                    j++;
                }
                //树到结尾
                if (pointer.getMap().isEmpty()) {
                    return true;
                }
                //树没有到结尾
                pointer = root;
            }
            return false;
        }
    }
    
    1. 使用validation直接用正则表达式
      ^(?!\d+$)(?![a-z]+$)(?![A-Z]+$)[\da-zA-z]{6,16}$
      GitHub地址:https://github.com/lei342345795/FromZerotoExpert
    0 赞同 0 条回复
  • So 普通 2022年10月21日 上午11:56

    1、考虑到高并发,采用redis分布锁
    2、

    package com.yang.util;
    
    import java.util.*;
    
    public class SensitiveWordUtil {
        public static Map dictionaryMap = new HashMap<>();
    
        public static void initMap(Collection words) {
            Map map = new HashMap<>(words.size());
            Map curMap = null;
            Iterator iterator = words.iterator();
            while (iterator.hasNext()) {
                String word = iterator.next();
                curMap = map;
                int len = word.length();
                for (int i =0; i < len; i++) {
                    String key = String.valueOf(word.charAt(i));
                    Map wordMap = (Map) curMap.get(key);
                    if (wordMap == null) {
                        wordMap = new HashMap<>(2);
                        wordMap.put("isEnd", "0");
                        curMap.put(key, wordMap);
                    }
                    curMap = wordMap;
                    if (i == len - 1) {
                        curMap.put("isEnd", "1");
                    }
                }
            }
            dictionaryMap = map;
        }
    
        private static int checkWord(String text, int beginIndex) {
            boolean isEnd = false;
            int wordLength = 0;
            Map curMap = dictionaryMap;
            int len = text.length();
            for (int i = beginIndex; i < len; i++) {
                String key = String.valueOf(text.charAt(i));
                curMap = (Map) curMap.get(key);
                if (curMap == null) {
                    break;
                } else {
                    wordLength++;
                    if ("1".equals(curMap.get("isEnd"))) {
                        isEnd = true;
                    }
                }
            }
            if (!isEnd) {
                wordLength = 0;
            }
            return wordLength;
        }
    
        public static boolean mathWords(String text) {
            int len = text.length();
            for (int i = 0; i < len; i++) {
                int wordLength = checkWord(text, i);
                if (wordLength > 0) {
                    return true;
                }
            }
            return false;
        }
    }
    

    3、

    package com.yang.util;
    
    import java.util.regex.Matcher;
    import java.util.regex.Pattern;
    
    public class PasswordUtil {
        private static final String REG_NUMBER = ".*\\d+.*";
        private static final String REG_UPPERCASE = ".*[A-Z]+.*";
        private static final String REG_LOWERCASE = ".*[a-z]+.*";
        private static final String REG_SYMBOL = "[ _`~!@#$%^&*()+=|{}':;',\\[\\]" +
                ".<>/?~!@#¥%……&*()——+|{}【】‘;:”“’。,、?]|\n|\r|\t";
        public static boolean checkPassword(String password) {
            Pattern pattern = Pattern.compile(REG_SYMBOL);
            Matcher matcher = pattern.matcher(password);
            if (password.length() < 6 || password.length() >16 || matcher.find()) {
                return false;
            }
            int i = 0;
            if (password.matches(REG_NUMBER)) {
                i++;
            }
            if (password.matches(REG_UPPERCASE)) {
                i++;
            }
            if (password.matches(REG_LOWERCASE)) {
                i++;
            }
            if (i < 2) {
                return false;
            }
            return true;
        }
    }
    

    github:https://github.com/Egofir/ferry

    0 赞同 0 条回复
  • lucca 普通 2022年10月8日 下午9:41

    做接下来两讲之前,我先去看了后面的大佬分享,从中获取了不少思路

    1、用户名去重:
    上个版本搭建注册的时候已经使用查数据库 + 唯一索引的方式做了去重。
    看了文章之后,使用redis实现分布式锁,防止同一个用户名被并发访问,同时每个获取锁的线程还设置了不同的value,防止锁的误解除,并设置了较长的过期时间减少因为执行流程过长而导致的并发执行问题。
    但我个人觉得,注册这个功能,在大部分场景下应该不能算一个高并发场景,就算是并发,同一个key被高并发访问的概率也是不大的,不过,造火箭嘛,嘿嘿。

    2、给出你敏感词过滤的算法
    看了帅地大佬的trie树思路,自己用基于计数的思路写了一个。
    封装成trie树类,方便后续复用(除了用于敏感词,还可以用于词频统计等场景)
    以下是代码:

    
    /**
     * Trie树 实现:
     * 1.关键字查找
     * 2.词频统计
     */
    public class TrieTree {
    
        private final TrieNode root = new TrieNode();
    
        /**
         * 插入关键词
         *
         * @param word 关键词
         */
        public void insert(String word) {
            // 如果是字符串不做处理
            if (StrUtil.isBlank(word)) return;
    
            char[] cs = word.toCharArray();
            TrieNode t = this.root;
            for (char c : cs) {
                if (!t.child.containsKey(c)) {
                    t.child.put(c, new TrieNode());
                }
                t = t.child.get(c);
                t.cnt++;
            }
            t.end++;
        }
    
        /**
         * 删除字符串,回收空闲节点
         *
         * @param word 字符串
         */
        public void delete(String word) {
            if (search(word) == 0) return;
    
            TrieNode t = root;
            char[] cs = word.toCharArray();
            for (char c : cs) {
                if (--t.child.get(c).cnt == 0) {
                    t.child.remove(c);
                    return;
                }
                t = t.child.get(c);
            }
            t.end--;
        }
    
        /**
         * 返回单词频率
         *
         * @param word 单词
         * @return 频率
         */
        public int search(String word) {
            if (StrUtil.isBlank(word)) return 0;
    
            TrieNode t = root;
            char[] cs = word.toCharArray();
            for (char c : cs) {
                if (!t.child.containsKey(c)) return 0;
                t = t.child.get(c);
            }
            return t.end;
        }
    
        public boolean exist(String word) {
            if (StrUtil.isBlank(word)) return false;
    
            TrieNode t = root;
            char[] cs = word.toCharArray();
            for (int i = 0; i < cs.length; i++) {
                t = root;
                int p = i;
                while (p < cs.length && t.child.containsKey(cs[p])) {
                    t = t.child.get(cs[p++]);
                    if (t.end > 0) return true;
                }
            }
            return false;
        }
    
        // 节点
        private static final class TrieNode {
            int cnt;    // 前缀频率
            int end;    // 该字符串出现次数
            HashMap child;
    
            public TrieNode() {
                cnt = 0;
                end = 0;
                child = new HashMap<>();
            }
        }
    }
    

    3、给出你限制密码强度/长度/字符的算法
    本来是想写一个注解来实现这个功能的,发现要是用注解来实现的话,如果想分情况为前端返回非法密码的情况,将要写很多注解,只是为了一个参数校验的话,我觉得是很浪费的,因此还是改为使用工具类实现了,同时借此机会将代码所有的常量和枚举重新整理了一下,密码校验的代码比较简单:

    public class PasswordUtil {
    
        public static PasswordEnum checkValidity(String password) {
            // 密码太短或太长
            if (password.length() < MIN_PASSWORD_LEN) return PASSWORD_TO_SHORT;
            if (password.length() > MAX_PASSWORD_LEN) return PASSWORD_TO_LONG;
    
            // 非法字符
            if (!password.matches("[0-9a-zA-Z]{6,16}")) return INVALID_CHAR_TYPE;
            // 字符种类太少
            if (password.matches("([0-9]*)|([a-z]*)|([A-Z]*)")) return TO_FEW_CHAR_TYPE;
    
            return PERFECT_PASSWORD;
        }
    }
    

    枚举

    public enum PasswordEnum {
    
        PERFECT_PASSWORD(0, "合法密码"),
        PASSWORD_TO_SHORT(1, "密码过短"),
        PASSWORD_TO_LONG(2, "密码过长"),
        INVALID_CHAR_TYPE(3, "密码字符只能是数字+英文大小写"),
        TO_FEW_CHAR_TYPE(4, "密码必须包含两种及其以上字符"),
        ;
    
        private int code;
        private String message;
    
        PasswordEnum(int code, String message) {
            this.code = code;
            this.message = message;
        }
    }
    

    github地址:https://github.com/lucca-suen/FromZerotoExpert/tree/v2.2

    1 赞同 0 条回复
  • 潜行1 我是火车王! 普通 2022年10月3日 下午1:49
    1. 用户名重复
      限制用户名重复的话我和 月诉长安 同学的思考过程差不多,因为注册是个并发量大的模块,所以最麻烦的就是查询失效的问题,暂时也想不到怎么搞;
      暂时只用axios弄了个最基本的异步查询出来:就是后端直接返回字符串,然后前端控制显示在指定的位置。
    2. 敏感词判断
      功能最简;所以不会实际拦截不合要求的请求
      看的大家都用的前缀树,我自己也是想的前缀树,所以 项目里面的实现弄一个前缀树实现;
      项目里面的代码就用别人的Java代码了,反正实现都差不多

    3. 密码强度判断
      密码强度的限制的话差不多也是用正则判断,我自己也想不到其他的了 >_<
      前端的部分和第一个差不多,所以没做;
      密码判断的逻辑在打卡区找了一个正则表达式的,直接参考的他的,所以没写

    项目地址: https://github.com/Runninginsilence1/FromZero2Expert/tree/dev

    0 赞同 0 条回复
  • 月诉长安 永久会员 2022年9月23日 下午12:50

    1.实现防止用户重名:
    一开始我没想这么多,简单的查询返回。后来在网上论坛看相关的讨论有了以下的理解。主要容易出问题的场景就是“高并发”,为了防止高并发下用户名查询查重速度变慢,查重就会失效。以我现在的水平我只想到了加锁,而且是最简单的在查找逻辑那里加锁,论坛讨论是加分布锁(没听说过),我了解了分布式知道了原因,一般选用Redis实现分布式锁(又是盲区),锁的范围也不能简单粗暴的包括整个方法,要降低锁的粒度。然后就是对Redis实现的详细讨论,我毕竟没学过也没看懂。另一种方式就是将username字段设置成唯一属性,将错误丢给数据库处理,论坛说:
    “但唯一约束会严重制约扩展能力(读写分离、分库分表),同时降低系统的健壮性(通过数据库报错来反馈业务错误信息)。除了唯一索引,还可以锁表,但是我觉得任何一个有责任心的程序员都不会这样实现。”
    至于为什么会导致这么多问题,我也不太清楚。到此,一个小小的查重,能反映我有多么菜···,这些问题我先标记一下,在以后的学习中,我会解决心里的疑惑。

    2.敏感词审查。
    第一步:将敏感词的词库转换成利于敏感词检测的数据结构,我感觉就是转成森林,森林中每一个树都是开头字符相同的一类敏感词,节点是敏感词的单个字符,根结点事敏感词的开头字母。每个节点都有状态表示,endl=1 表示从root节点到此节点一个敏感词结束

    第二步:在java中用hashmap来实现这个数据结构:如上图的数据结构可以表示为:

    {<丝, {<袜, {<endl, 1>}>, <endl, 1>}>, <end, 0>}
    {<黄, {{<色, {<丝, {<袜,{<end,1>}>>, <end, 1>}>, < endl, 0>}>,<endl, 0>},{<赌, {<毒,{<endl, 1>}>,<endl, 1>}>, <endl, 0>}}>, <end, 0>}

    第三步:代码实现,我就不放代码了,我尝试实现出很多问题,功力不够,我在网上找的源码,读的他的源码复现的
    3.密码格式判断

    package com.shk.utils;
    
    /**
     * @author: sunhengkang
     * @date:2022/9/22
     */
    public class CheckPasswordUtil {
        //数字
        public static final String NUMBER = ".*\\d+.*";
        //小写字母
        public static final String UPPERCASE = ".*[A-Z]+.*";
        //大写字母
        public static final String LOWERCASE = ".*[a-z]+.*";
        //特殊符号
        public static final String SYMBOL = ".*[~!@#$%^&*()_+|<>,.?/:;'\\[\\]{}\"]+.*";
    
        /**
         *
         * @param password
         * @return 0:密码格式正确 1.长度违规 2.含有特殊字符 3.数字,小写,大写不足两个
         */
        public static int checkPasswod(String password){
            int passwordLenth = password.length();
            //判断密码长度
            if(passwordLenth < 6 || passwordLenth > 15){
                return 1;
            }
    
            //判断是否由字母,数字,特殊字符组成
            int i = 0;
            if (password.matches(NUMBER)) i++;
            if (password.matches(LOWERCASE))i++;
            if (password.matches(UPPERCASE)) i++;
            if (password.matches(SYMBOL)) return 2;
            if (i  < 2 )  return 3;
            return 0;
    
        }
    }
    
    
    2 赞同 1 条回复
  • xiaoxi 永久会员 2022年9月22日 下午11:36

    1.我是首先在数据库的user_name字段上加了个唯一索引,然后在注册账号接口,也就是业务层先查询数据库中是否已存在这个昵称来判断是否执行接下来的操作,既然已经在业务层判断昵称是否重复,我认为还是有必要在数据库中再判断下,因为我认为用户可以通过在前端页面找到后端的注册账号接口,并摸清数据格式,然后向注册接口。。。,好像只需在业务层判断就行,唯一索引没有必要,我认为另外还需对后端接口的参数验证,前端也需验证,前端验证主要是防止普通用户输入错误的参数,而后端对数据进行验证,是放在一些人恶意向接口发送一些错误的数据。
    2.敏感词过滤,用前缀树(HashMap实现)实现

    public class Trie {
        class TrieNode {
            // 字符为键,树节点为值
            private HashMap children = new HashMap<>();
            // 判断是否是树的叶子节点
            private boolean isEnd;
        }
        private TrieNode root;
        public Trie() {
            root = new TrieNode();
        }
        /** 插入一个字符串 */
        public void insert(String word) {
            TrieNode cur = root;
            for (int i = 0; i < word.length(); i ++) {
                char c = word.charAt(i);
                if (!cur.children.containsKey(c)) {
                    cur.children.put(c, new TrieNode());
                }
                cur = cur.children.get(c);
            }
            cur.isEnd = true;
        }
        /** 得到前缀搜索得到的树节点 */
        private TrieNode searchPrefix(String prefix) {
            TrieNode cur = root;
            for (int i = 0; i < prefix.length(); i ++) {
                char c = prefix.charAt(i);
                if (cur.children.containsKey(c)) {
                    cur = cur.children.get(c);
                } else {
                    return null;
                }
            }
            return cur;
        }
        /** 搜索前缀树是否存在字符串word */
        public boolean searchWord(String word) {
            TrieNode cur = searchPrefix(word);
            return cur != null && cur.isEnd == true;
        }
        /** 搜索前缀树中是否存在前缀prefix */
        public boolean startPrefix(String prefix) {
            TrieNode cur = searchPrefix(prefix);
            return cur != null;
        }
        public static Trie constructTrie(String[] words) {
            Trie trie = new Trie();
            for (int i = 0; i < words.length; i ++) {
                trie.insert(words[i]);
            }
            return trie;
        }
    }
    
    1. 对于验证,昵称、账号、密码我是写了个VaildUtil工具类验证,邮箱格式验证0我是用@Vaild注解实现
    public class ValidUtil {
        private static String[] words = {"尼玛", "sb", "草泥马"};
        private static Trie trie = Trie.constructTrie(words);
    
        /** 判断用户账号是否符合要求 */
        public static boolean isVaildAccount(String account) {
            // 账号为空,抛出异常
            if (account == null) {
                throw new UserException(UserExceptionEnum.ACCOUNT_IS_NULL);
            }
            // 账号长度不符合,返回false
            if (account.length() < 6 || account.length() > 16) {
                return false;
            }
            // 保护除数字、字母外的特殊字符,返回false
            for (char c : account.toCharArray()) {
                if(!isNumber(c) && !isCharacter(c)) {
                    return false;
                }
            }
            return true;
        }
        /** 判断用户昵称是否符合要求 */
        public static boolean isVaildName(String name) {
            // 昵称为空,抛出异常
            if (name == null) {
                throw new UserException(UserExceptionEnum.NAME_IS_NULL);
            }
            // 昵称长度不符合,返回false
            if (name.length() < 2 || name.length() > 16) {
                return false;
            }
            return true && !trie.startPrefix(name);
        }
        /** 判断用户密码是否符合规范*/
        public static boolean isVaildPassword(String password) {
            if (password == null) {
                throw new UserException(UserExceptionEnum.PASSWORD_IS_NULL);
            }
            // 密码中是否含数字
            boolean hasNum = false;
            // 密码中是否含字母
            boolean hasCharacter = false;
            for (char c : password.toCharArray()) {
                if (isNumber(c)) {
                    hasNum = true;
                }
                if (isCharacter(c)) {
                    hasCharacter = true;
                }
            }
            return hasNum && hasCharacter;
        }
    
        private static boolean isNumber(char c) {
            return c >= '0' && c <= '9';
        }
        private static boolean isCharacter(char c) {
            return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
        }
    }
    

    注册接口代码:

    @RestController
    @Slf4j
    @Validated
    public class RegisterController {
        @Autowired
        private UserService userService;
    
        @PostMapping("/register")
        private ResultVO register(@RequestBody @Valid RegisterForm registerForm, BindingResult bindingResult) {
            if (bindingResult.hasErrors()) {
                return ResultVOUtil.fail(1,"注册参数不正确");
            }
            if (!ValidUtil.isVaildName(registerForm.getUserName())) {
                log.error("昵称不符合");
                return ResultVOUtil.fail(2, "昵称不符合");
            }
            if (!ValidUtil.isVaildAccount(registerForm.getUserAccount())) {
                log.error("账号不符合");
                return ResultVOUtil.fail(3, "账号不符合");
            }
            if (!ValidUtil.isVaildPassword(registerForm.getUserPassword())) {
                log.error("密码不符合");
                return ResultVOUtil.fail(4, "密码不符合");
            }
            if (userService.getOneByName(registerForm.getUserName()) != null) {
                log.error("昵称已存在");
                return ResultVOUtil.fail(5, "昵称已存在");
            }
            if (userService.getOneByAccount(registerForm.getUserAccount()) != null) {
                log.error("账号已存在");
                return ResultVOUtil.fail(6, "账号已存在");
            }
            User user = new User();
            BeanUtils.copyProperties(registerForm, user);
            userService.addOne(user);
            return ResultVOUtil.success();
        }
    }
    

    github地址:https://github.com/shuaishuaixi03/FromZerotoExpert/tree/dev

    1 赞同 0 条回复
  • 格局 永久会员 2022年9月21日 下午8:36

    1、限制用户名重复,在注册接口那里去数据库中查找username,是否有相同的值,然后返回状态码,前端通过Ajax进行接收,显示到界面;


    2、敏感词过滤:也是采用前缀树这种数据结构进行过滤的,之前也没学过这个数据结构,就趁这次机会学习了一下,选择前缀树也是因为其利用字符串的公共前缀来降低查询时间,来提高搜索效率。

    利用文件IO的知识,将文本文件中的敏感词读取出来,然后存入前缀树的节点中,

    然后利用指针来读取节点的值,过滤传入的值是否为敏感词


    3、密码长度、强度的限制:使用正则表达式进行匹配

    然后在注册接口处使用来限制用户名的输入

    GitHub链接:https://github.com/zzxjava/FromZerotoExpert/tree/dev

    0 赞同 0 条回复
  • 秊 永久会员 2022年9月21日 上午10:31
    1. 限制用户昵称重复:在建表时增加 unique key username_unique (username) 语句
    2. 敏感词过滤算法:使用 Trie
      A.首先构造Trie类:
    public class Trie {
    
        // 每个Trie的数据内容为上一个Trie的children,自身是否为end是当前自己的 isEnd标志
        private HashMap children;
        private boolean isEnd;
    
        public Trie() {
            children = new HashMap();
            isEnd = false;
        }
    
        public void insert(String singleWord) {  // 先插入后匹配
            Trie node = this;
            for (int i = 0; i < singleWord.length(); ++i) {
                char ch = singleWord.charAt(i);
                if (!node.children.containsKey(ch)) {
                    node.children.put(ch, new Trie());
                }
                node = node.children.get(ch);
                if (i == singleWord.length() - 1) {
                    node.isEnd = true;
                }
            }
        }
    
        /**
         *  检测整串昵称是否含有敏感词汇
         * @param target 目标字符串
         * @param root 敏感词汇Trie
         * @return true - 是; false - 否
         */
        public boolean check(String target, Trie root) {
            for (int p1 = 0; p1 < target.length(); ++p1) {
                char ch = target.charAt(p1);
                Trie node = root;
                if (root.children.containsKey(ch)) {
                    node = root.children.get(ch);
                    if (node.isEnd) {
                        return true;
                    } else {
                        for (int p2 = p1 + 1; p2 < target.length(); ++p2) {
                            char cch = target.charAt(p2);
                            if (node.children.containsKey(cch)) {
                                node = node.children.get(cch);
                                if (node.isEnd) {
                                    return true;
                                }
                            } else {
                                break;   // 匹配中断后中返回p1继续往后匹配
                            }
                        }
                    }
                }
            }
            return false;
        }
    }
    
    B. 使用`UserNameLimit`的静态方法`userNameLimit`传入用户名进行判断:
    
    /**
         * 昵称限制
         * @param userName
         * @return 0 - 昵称符合; 1 - 含有敏感词
         */
        public static int userNameLimit(String userName) {
            List impolitePhrases = new ArrayList(Arrays.asList("尼玛","站长","国家领导人","操"));
    
            // 敏感词判断
            Trie root = new Trie();
            for (String s : impolitePhrases) {
                root.insert(s);
            }
            if (root.check(userName, root)) {  // 传入敏感词Trie
                return 1;
            }
            return 0;
        }
    
    1. 密码强度限制, 使用的正则表达式进行匹配:
    public class PasswordLimit {
    
        /**
         * 密码限制
         * @param password 传入的密码
         * @return 0 - 密码符合; 1 - 密码长度只能在 6 - 16 位; 2 - 密码只能包含数字和英文大小写三种字符; 3 - 密码需要至少两种字符
         */
        public static int passWordLimit(String password) {
    
    
    
            // 长度判断
            if (password.length() < 6 || password.length() > 16) {
                return 1;
            }
    
            // 强度判断: 密码字符只能是 数字+英文大小写(算是三种字符); 包含 2 种及其以上字符;
            int wordType = 0;
            Pattern pattern1 = Pattern.compile("^[a-zA-Z0-9]*$"); // 只能匹配前者
            Pattern pattern2 = Pattern.compile("^(?![a-z]+$)(?![A-Z]+$)(?![0-9]+$)[a-zA-Z0-9]*$"); // 只能匹配后者
            if (!pattern1.matcher(password).matches()) {
                return 2;
            }
            if (!pattern2.matcher(password).matches()) {
                return 3;
            }
    
            return 0;
        }
    }
    

    ps: 为了简化Controller层,具体判断昵称和密码以及设置其对应的返回结果都在Service层的两个函数isUserName( )和isPassword( )完成, 注意对结果的修改可以通过传入 CommonResp的引用动态修改

    服务器链接:http://124.223.184.187:8088/FromZerotoExpert
    Github仓库链接:https://github.com/jiangyklala/FromZerotoExpert/tree/dev

    0 赞同 4 条回复
    • shuaidi 永久会员 @ 秊 2022年9月21日 上午11:15

      我看你用户插入时,是否插入成功,好像没有给个返回值之类的哦。另外就是关于防重复的设计,也可以参考下我总结的方案哦。

    • 秊 永久会员 @ shuaidi 2022年9月21日 上午11:24

      我在 Controller 中做了逻辑判断

      /**
           * 注册接口
           */
          @PostMapping("/register")
          @ResponseBody
          public CommonResp register(@Validated User user) {
              CommonResp<User> resp = new CommonResp<>();
              userService.isUserName(user.getUsername(), resp);
              userService.isPassword(user.getPassword(), resp); 
              if (resp.isSuccess()) {  // 如果以上两个函数都成功, resp.getSuccess( )即为 true,插入用户,否则不插入,且其对应的message中含有相应的错误信息
                  userService.addUser(user);
                  resp.setContent(user);
              }
              return resp;
          }
      

      以下是UserService中两个函数的实现

      /**
           *
           * @param userName
           * @param resp 传入最终返回结果类的引用, 进行修改
           */
          public void isUserName(String userName, CommonResp resp) {
              switch (UserNameLimit.userNameLimit(userName)) {
                  case 1 -> {
                      resp.setSuccess(false);
                      resp.setMessage(resp.getMessage() + "含有敏感词; ");  // 原有的信息也不要丢
                  }
                  case 0 -> {
                      if (resp.isSuccess()) {  // 是否已经是 false 了??
                          resp.setMessage(resp.getMessage() + "昵称符合限制; ");
                      }
                  }
              }
          }
      
          /**
           *
           * @param password
           * @param resp 传入最终返回结果类的引用, 进行修改
           */
          public void isPassword(String password, CommonResp resp) {
              switch (PasswordLimit.passWordLimit(password)) {
                  case 3 -> {
                      resp.setSuccess(false);
                      resp.setMessage(resp.getMessage() + "密码需要至少两种字符; ");
                  }
                  case 2 -> {
                      resp.setSuccess(false);
                      resp.setMessage(resp.getMessage() + "密码只能包含数字和英文大小写三种字符; ");
                  }
                  case 1 -> {
                      resp.setSuccess(false);
                      resp.setMessage(resp.getMessage() + "密码长度只能在 6 - 16 位; ");
                  }
                  case 0 -> {
                      if (resp.isSuccess()) {
                          resp.setMessage(resp.getMessage() + "密码符合限制; ");
                      }
                  }
              }
          }
      
    • 秊 永久会员 @ shuaidi 2022年9月21日 上午11:27

      关于redis,分布式锁目前还没学到?,慢慢补

    • shuaidi 永久会员 @ 秊 2022年9月21日 上午11:34

      可以哦,对于还没有学过的,按照最原始的方式来实现就行,以后可以回过头来补滴。

      另外就是,你的代码其实我有看了一下,就是对于 userService.addUser(user);,用户在插入的时候,好像没看到插入成功或者失败的代码。比如用户名重复了,唯一字段导致插入失败,你这个时候的返回值是啥啊

  • Second(S) 普通 2022年9月16日 上午8:48

    本次功能主要是校验信息,如校验用户名(冲突、长度、敏感)以及校验密码(长度、强度、字符),因此在建立校验算法之前,先提前构建一个RegisterResult类来返回校验结果,供前端接收统一处理,其中RegisterResult类包含两个属性,isQualified属性(布尔类型,代表校验成功/失败)和message属性(String类型,返回失败时的信息,如“用户名含有敏感词”)。
    1.用户名重复校验

    在username输入框中进行离焦检测(blur),当离焦时,用ajax异步提交到controller中的usernameCheck()方法进行检测,检测重复的算法也很简单,就是在数据库中查询username,看看是否能返回User对象,若返回为null则数据库中没有重复的用户名,否则用户名重复,让前端页面给出提示。注:用户名重复的检测需要提交到数据库速度可能会慢一些,所以应该先对用户名的长度、敏感词进行检测,若不符合,就不用提交到数据库进行查重检测了,若长度和敏感词全都符合要求,则再进行查重处理,可能这样就会节省一点时间。(自己想的,不知道对还是错)。
    2.敏感词校验

        /**
         * 敏感词检测
         * @param username 用户名
         * @param root 已经构建好的字典树
         * @return
         */
        public static RegisterResult checkDisalowWord(String username, Trie root){
            int i = 0;
            while(i < username.length()){
                int j = i;
                // 若username子串前缀不为敏感词前缀,一定不是敏感词
                // 若子串前缀为敏感词前缀,则继续检测是否为敏感词,若是,则返回false和信息否则继续检测,直至跳出循环。
                while(j < username.length() && root.startsWith(username.substring(i, j + 1))){
                    if(root.search(username.substring(i, j + 1))){
                        return new RegisterResult(false, "用户名含有敏感词汇!");
                    }
                    j++;
                }
                i++;
            }
            return new RegisterResult(true);
        }
    

    首先写前缀树的代码,因为之前leetcode编写过这道题,所以能直接写出代码,然后构建一个记录敏感词的数组,这里是在工具类中定义的一个静态成员数组sensitiveWords,有点写死了,然后建立前缀树对象并将敏感词插入,然后将用户名传入进行检测,具体方式为逐个字符判断后续子串是否为敏感词的前缀,若不是,则不可能为敏感词,进行下一个字符的检测。若是,则加长子串,检测直至前缀为敏感词且子串为敏感词时,则该用户名包含敏感词。
    注:这里其实用帅地哥分享的p1,p2,p3三个指针遍历前缀树检测时间复杂度会少一些,但是我想着用户名长度一共也就15,就没太在意复杂度了,就写的有点暴力,这里后面可以优化的。
    3.密码校验

    /**
         * 判断用密码是否为空,长度是否在6~16之间,以及是否包含两种以上字符,且只包含数字及英文大小写
         * @param password
         * @return RegisterResult
         */
        public static RegisterResult checkPassword(String password){
            // 判断密码是否为null, 防止空指针异常
            if(null == password || password.length() == 0) return new RegisterResult(false, "密码不允许为空!");
    
            // 判断密码长度是否在6~16之间
            if(password.length() < 6 || password.length() > 16) return new RegisterResult(false, "密码长度必须为6~16位!");
    
            // 判断是否包含两种以上字符,且只包含数字及英文大小写
            int typenum = 0;
            boolean capital = false;
            boolean small = false;
            boolean digital = false;
            for(int i = 0; i < password.length(); i++){
                char c = password.charAt(i);
                if(c >= 'a' && c <= 'z'){
                    small = true;
                }else if(c >= 'A' && c <= 'Z'){
                    capital = true;
                }else if(c >= '0' && c <= '9'){
                    digital = true;
                }else{
                    return new RegisterResult(false, "密码只能为大写字母,小写字母或数字!");
                }
            }
            if(capital) ++typenum;
            if(small) ++typenum;
            if(digital) ++ typenum;
            if(typenum < 2) return new RegisterResult(false, "密码必须由至少两种字符组成!");
    
    
            return new RegisterResult(true);
        }
    

    这个算法还是挺简单的,具体如图所示在这里不进行过多的介绍了。
    gitHub路径:https://github.com/Second6s9/FromZerotoExpert/tree/dev
    gitHub防覆盖路径:https://github.com/Second6s9/FromZerotoExpert/tree/day06
    服务器地址:http://39.101.66.57:8080/FromZerotoExpert/register

    1 赞同 3 条回复
    • shuaidi 永久会员 @ Second(S) 2022年9月16日 下午12:02

      实际上 ,关于 用户名 重复,是一类实际项目中非常 常见的问题,归属「防重」这一类问题,针对这个问题,在面试可能面试官就会和你谈 造火箭的。

      我先抛一些问题给你,后面我会给出解决方案。

      1、比如通过查询数据库判断重复,那高并发下怎么办?这样会出现重复
      2、有人可能会说,我可以用唯一字段约束,这确实是最省事的,但是我们不应该把问题抛给数据库去处理,应该是业务层处理,而且用了唯一约束,其实会影响后续的拓展,毕竟改表操作是一个很危险的操作。
      3、有人可能会说,我可以用 sychronized 加锁的,但是sychronized 只能锁单机,而且我们还得保证锁的力度尽量小。
      4、好吧,我 用分布式锁吧,那用啥作为分布式锁?最简单的就是 redis,但是redis有很多抗,可能面试官还会让你手写一个。
      5、好吧,用redis来分布式锁,那 key 应该是什么呢?怎么设置key才更加高效呢?
      ……
      这些,都能 成为连环炮,当然,我们不求最优解,只求 思考过,给出面试官一个合理的答复。

      不过这些,我会慢一点讲吧,主要好多人没跟上。

    • Second(S) 普通 @ shuaidi 2022年9月16日 下午9:58

      谢谢帅地哥提供的思路,首先,坦率的讲,我在完成这个任务的时候完全没有考虑到高并发情况的,如果在真正业务中存在高并发需求的话,以我目前的思路,只能想到用唯一字段或使用sychronized加锁的方式去解决,但这两种方法貌似都并不是好的解决方案。并且对于redis以及分布式锁,我只是在别人的项目中看到过,或是自己简单的拿来套用一下,理解的层面并不是很深入,只能说是能看懂别人是怎么实现的,自己实现则完全没有思路,于是我就自己简单的查了一下「防重」问题的解决方案,整理的思路如下:
      1.给表设置唯一索引
      这样做的好处是简单粗暴成本低,但是会有一些缺点,就是我们很多的业务表中数据的删除操作一般并不是物理删除,而是逻辑删除操作(设置删除状态码),对于这样的表,设置唯一索引可能会带来一些坏处,比如当我们进行了逻辑删除操作时,我们只是将删除状态码进行了改变,但数据依然存在,可是当我们要插入这个已经删除了的数据时,会发生冲突(因为还在表中,只是改变了状态码),所以这种情况设置唯一索引是不可行的。
      2.redis分布式锁
      基本原理是在数据库执行添加操作前,进行加锁,如果加锁成功,则检测是否重复,若不重复则进行添加。分布式锁的目的主要是保证了查询操作和添加操作的原子性,但是这种方式也是有一些弊端的,比如当面临大量的数据添加时,每一次的添加操作都需要加锁才能完成,这将大大影响性能,很明显,分布式锁只适用于少量的添加操作,对于批量接口,加redis分布式锁,不是一个理想的方案。
      3.防重表
      对于1中的方案,在逻辑删除的表中不可行,而2中的方案在面临大量的添加操作时,性能方面也会存在问题,因此可以换一种思路,就是构建一张防重表,表中的数据记录了需要查重的字段,并设置为唯一字段,当进行添加操作时,先在防重表中添加数据,如果添加成功,则说明没有重复,否则说明数据重复,可以通过异常捕获进行处理。并且当原表进行逻辑删除操作时,将防重表进行物理删除,这样就解决了之前的问题。
      以上是我查到的一些思路,说实话,受益匪浅,自己在做项目的时候完全想不到这么多,并且在查的过程中还了解到了一些如何在高并发情况下保证接口幂等性的相关解决方案,真的是越深入了解,越发现自己不懂的还是太多。总之,路漫漫其修远兮,还需要加倍努力的呀!

    • shuaidi 永久会员 @ Second(S) 2022年9月17日 上午10:50

      没有考虑正常的,做的时候也不用去高并发做,我就等大家做完跟大家说一说 造火箭 勒,基于面试喜欢造火箭的方式来。

      另外就是,对于redis分布式锁,有个可以优化的方案,每次加锁时 Key 都设置成不一样的,就可以增加效率了,因为只有 用户名 相同时,才需要获取锁,我们的 key 可以用 前缀 + 用户标识 的方式,这样效率就会很高的,基本不用等待锁,因为重复名的还是很少的。

      但是这样会有一个问题,就会有大量 key,对于这种情况,我们可以想个策略删除他们,比如用户插入成功后,就删除这个key。

# 回答此问题

后才能回答