在前后端分离架构中使用 CAS+JWT 认证

原因

  • CAS ticket 是一次性的,验证后就失效
  • 前后端分离架构中,后续的 API 请求需要一个持续有效的身份凭证
  • JWT token 可以包含用户信息,减少数据库查询
  • JWT 支持客户端存储,适合前后端分离架构

时序图

image

简而言之,用户前端跳转到CAS服务器验证登录,登录URL为 http://localhost:8081/cas/login?service=http%3A%2F%2Flocalhost%3A8080%2Fauth%3Fredirect_url%3Dhttps%253A%252F%252Flocalhost%253A5173%252Fmember

登录完成后CAS携带ST令牌回调到service页面,URL为 http://localhost:8080/auth?redirect_url=https%3A%2F%2Flocalhost%3A5173%2Fmember&ticket=ST-1-Ns-YnfPvYpd5jvEkJah7p-hXU7E-aeaa5f03d463

后端service收到请求后向CAS服务器验证ST令牌,令牌授权后准备下发jwt token作为持久性鉴权。一种实现方法是:302跳转至redirect_url,同时跟上路由参数token;前端处理token写入浏览器存储

代码实现

CAS验证处理函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func AuthHandler(c *gin.Context) {
casURL := c.MustGet("casURL").(string)
jwtSecret := c.MustGet("jwt_secret").(string)
db := c.MustGet("db").(*gorm.DB)
redirectURL := c.Query("redirect_url")
u, _ := url.Parse(casURL)
client := cas.NewClient(&cas.Options{URL: u})

h := client.HandleFunc(func(w http.ResponseWriter, r *http.Request) {
if !cas.IsAuthenticated(r) {
client.RedirectToLogin(w, r)
} else {
// jwt
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{})
tokenString, err := token.SignedString([]byte(jwtSecret))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
} else {
c.Redirect(http.StatusFound, redirectURL+"?token="+tokenString)
}
}
})
h.ServeHTTP(c.Writer, c.Request)
}

设置路由

1
2
3
func SetupAuthRouters(router *gin.Engine) {
router.GET("/auth", handlers.AuthHandler)
}

JWT中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func JWTMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 校验jwt
token := c.GetHeader("Authorization")
jwt_secret := c.MustGet("jwt_secret").(string)
if token != "" && len(token) > 7 {
token = token[7:] //不含bearer
parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
return []byte(jwt_secret), nil
})
if err != nil {
c.AbortWithError(401, err)
return
}
if !parsedToken.Valid {
c.AbortWithStatus(401)
return
}
} else {
c.AbortWithStatus(401)
return
}
// 继续处理请求
c.Next()
}
}