用户认证的实践指南 | 青训营笔记

111 阅读4分钟

如何将我的服务开放给用户 :用户认证的实践指南 | 青训营笔记

用户认证在后端系统中是非常重要且必要的组成部分,它们在保障系统安全性、数据完整性、用户隐私以及提供良好用户体验方面发挥着关键作用

​ 用户认证是保护系统数据安全的重要措施,。通过认证,系统可以确认用户的身份,限制未经授权的用户访问敏感数据或执行特定操作,确保数据不被恶意篡改或删除。用户认证可以确保用户的个人信息得到保护,只有经过身份验证的用户才能访问和修改自己的信息。用户认证可以识别用户身份,提供个性化的用户体验,如自定义设置、历史记录、推荐内容等,提高用户满意度和用户粘性。如果要将一个后端服务或者系统开放给用户使用,那么用户认证功能必不可少。

用户认证

​ Token是一种用于身份验证和授权的令牌。在用户认证流程中,使用Token的方式是为了实现无状态的认证,提高系统的扩展性和安全性,减轻服务器的压力,方便系统的扩展。在系统的设计中可以考虑采用一下的方法实现 基于token 的认证

  1. 用户登录时往后端发送账号和密码,服务端使用 JWT 生成 token 进行保存

    JWT(JSON Web Token)是一种用于生成Token的开放标准(RFC 7519),它使用JSON对象来表示Token,并使用数字签名或加密保证Token的可信性和安全性

    下面是 用 Go 封装的JWT

    type UserClaims struct {
    	CommonEntity
    	Name string `json:"name"`
    	jwt.StandardClaims
    }
    
    var myKey = []byte("douyin")
    
    // GenerateToken
    // 生成 token
    func GenerateToken(name string, commonEntity CommonEntity) (string, error) {
    	UserClaim := &UserClaims{
    		CommonEntity: commonEntity,
    		Name:         name,
    		//IsAdmin:        isAdmin,
    		StandardClaims: jwt.StandardClaims{},
    	}
    	token := jwt.NewWithClaims(jwt.SigningMethodHS256, UserClaim)
    	tokenString, err := token.SignedString(myKey)
    	if err != nil {
    		return "", err
    	}
    	return tokenString, nil
    }
    
    // AnalyseToken
    // 解析 token
    func AnalyseToken(tokenString string) (*UserClaims, error) {
    	userClaim := new(UserClaims)
    	claims, err := jwt.ParseWithClaims(tokenString, userClaim, func(token *jwt.Token) (interface{}, error) {
    		return myKey, nil
    	})
    	if err != nil {
    		return nil, err
    	}
    	if !claims.Valid {
    		return nil, fmt.Errorf("analyse Token Error:%v", err)
    	}
    	return userClaim, nil
    }
    
    
  2. 生成 token 后, 保存到 redis 后返回给用户 。分布式系统中,可能存在多个子系统或服务,用户可能需要在这些子系统之间实现单点登录。通过将Token存储在Redis中,可以实现不同子系统之间的Token共享,从而实现用户在一个子系统登录后,其他子系统无需再次登录。因此考虑将 token 保存 到 redis 中

    func SaveTokenToRedis(username string, token string, expiration time.Duration) error {
    	client := GetRedisDB()
    	ctx := context.Background()
    	key := fmt.Sprintf("%v%v", config.TokenKey, username)
    	err := client.Set(ctx, key, token, expiration).Err()
    	if err != nil {
    		return err
    	}
    	return nil
    }
    
  3. 用户访问其它页面时携带token , 这时候需要一个拦截器对用户的请求进行拦截。因为要校验该用户是否有权限访问这个地址。基本思路是使用 JWT 解析用户的token 然后,使用解析出来的账户名去 redis 中查出储存的 token , 如果能查出来 token ,说明用户已经登录,则可以放行。

    看起来这样已经很完美了,但是却隐藏了一个问题:由于token有有效期,就会出现一个情况,用户很频繁访问系统的时候,token 过期了。针对这个问题,要使得用户每请求一次就刷新一次 token 时间。因此设计两层 拦截器,第一层负责刷新 token 时间,一律放行。第二层拦截器负责对未登录且访问要登录的路径的请求进行拦截。

    // 免登录接口列表
    var notAuthArr = map[string]string{
    	"/douyin/feed/":          "1",
    	"/douyin/user/register/": "1",
    	"/douyin/user/login/":    "1",
    }
    
    /*
    *
    token刷新
    */
    func RefreshHandler() gin.HandlerFunc {
    	return func(c *gin.Context) {
    		//1.获取token
    		token := c.Query("token")
    		//如果token为空则尝试从body中拿
    		if token == "" {
    			token = c.PostForm("token")
    		}
    		//2.判断是否携带token
    		if token == "" {
    			return
    		}
    		//3.解析token
    		userClaims, err := AnalyseToken(token)
    		if err != nil || userClaims == nil || userClaims.IsDeleted == 1 {
    			return
    		}
    		//4.根据token查redis
    		tokenFromRedis, err := GetTokenFromRedis(userClaims.Name)
    		if tokenFromRedis == "" {
    			//4.1 如果token可以被正确解析,重建redis缓存
    			err := SaveTokenToRedis(userClaims.Name, token, time.Duration(config.TokenTTL*float64(time.Second)))
    			if err != nil {
    
    				c.JSON(http.StatusForbidden, gin.H{"StatusCode": "1", "StatusMsg": "服务器异常"})
    				c.Abort()
    				return
    			}
    			return
    		}
    		//6.刷新token的有效期
    		err = RefreshToken(userClaims.Name, time.Duration(config.TokenTTL*float64(time.Second)))
    		if err != nil {
    			c.JSON(http.StatusOK, gin.H{"StatusCode": "1", "StatusMsg": "用户未登录"})
    			return
    		}
    		c.Next()
    	}
    }
    
    /*
    *
    登录校验
    */
    func AuthAdminCheck() gin.HandlerFunc {
    	return func(c *gin.Context) {
    		//1.不用登录的接口直接放行
    		//log.Println(c.Request.URL.Path)
    		inWhite := notAuthArr[c.Request.URL.Path]
    		if inWhite == "1" {
    			return
    		}
    		//2.获取token
    		token := c.Query("token")
    		//如果token为空则尝试从body中拿
    		if token == "" {
    			token = c.PostForm("token")
    		}
    		userClaims, err := AnalyseToken(token)
    		if err != nil || userClaims == nil || userClaims.IsDeleted == 1 {
    			c.JSON(http.StatusOK, gin.H{"StatusCode": "1", "StatusMsg": "用户未登录"})
    			//阻止该请求
    			c.Abort()
    			return
    		}
    		//3.根据token查redis
    		tokenFromRedis, err := GetTokenFromRedis(userClaims.Name)
    		if tokenFromRedis == "" || err != nil {
    			c.JSON(http.StatusOK, gin.H{"StatusCode": "1", "StatusMsg": "用户未登录"})
    			//阻止该请求
    			c.Abort()
    			return
    		}
    		c.Next()
    	}
    }
    
    
  4. 至此,登录以及鉴权已经完成,后端可以使用 token 解析出来的 用户信息 进行后续操作

敏感信息储存

用户上传的密码信息因为只需要在登录时用同样的加密方式加密匹配就行,因此可以使用非对称加密算法。(如 bcrypt)

encrypt, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)

而用户的 身份证号码, 手机号等需要查看的敏感信息可以采用可逆加密算法。

这样对敏感信息的处理,使得入侵者即便拿到了数据库,也很难得到有用的用户信息

屏幕截图 2023-08-03 221556.png