申请接入微软SSO(隐式)大概需要以下流程:
- 申请一个Azure AD。
- 申请一个新的应用程序注册&&填写配置。
- 书写前端(本次使用了官方的react示例来实现)
- 书写后端(本次采用了go后端)
OAuth登录流程
- 相当于是把微软发行的id_token代替了我们的账号系统中的账户密码来进行登录。
分步说明(实操举例)
申请一个Azure AD
没什么好说的,直接进入AzureAD官网(azure.microsoft.com)按照流程申请一个就行了。
申请一个新的应用程序注册
-
step1:进入Azure AD页面
-
step2:应用注册→新注册
-
step3:名字随便填;支持的账户可以先选最宽广的;选SPA;重定向URI先填个base的。
-
step4:多添加一条的重定向url(localhost:3000/redirect)。添加这个URI的原因是因为之后会用到官方的前端示例,实际上可以根据自己的项目要求添加重定向URI。
-
step5:增加API权限。API权限→添加权限→选择MicrosoftGraph→委托的权限→添加如图上的所有权限(之后可根据自己的了解和需求增减)
插曲:确认你的clientID(第一个红框)和tenantID(第二个红框)

书写前端(本次使用了官方的react示例来实现)
说明书:[前端react配置说明书]
step0:安装npm和git(建议chatgpt查看如何安装)
step1:克隆代码库
git clone https://github.com/Azure-Samples/ms-identity-javascript-react-tutorial.git
step2:移动到项目启动目录里
cd 1-Authentication\1-sign-in\SPA
step3:修改配置文件(参考下图)
vim src\authConfig.js
export const msalConfig = {
auth: {
clientId: '9ea52bac-f416-43cf-ad6e-88dffe7d0b1e',
authority: 'https://login.microsoftonline.com/9e55725a-626e-4339-bcf7-2b3e915426fa',
...
}
...
}
- clientId填clientId,authority的url最后的path改为tenantID。
step4:运行程序
npm start
step5:login获取token

- 需要用f12抓token的请求中的id_token,后端直接用。(因为用的官方提供的SPA,并没有前后端连起来,到时候手动把id_token传过去)
书写后端(go)
后端的目的:从id_token(JWT形式)里面获取到信息,并且使用微软颁布的公钥去验证JWT是否被篡改。当JWT验证成功时,再颁发自己系统的登录token就可以了。 相当于用微软颁发的id_token中的信息来充当账号,id_token合法充当密码。其他一切照旧无需改动。
go项目的依赖环境准备
# go项目中运行(我用的是golang直接新建项目,所以跳过了初始化项目的步骤)
go get -u github.com/dgrijalva/jwt-go
go get -u github.com/gin-gonic/gin
go get -u github.com/lestrrat-go/jwx
go get github.com/lestrrat-go/backoff/v2
go get github.com/lestrrat-go/jwx/[email protected]
书写后端程序逻辑
我们需要做的是:
- 写一个路由入口接收id_token(gin框架书写)
- 对id_token进行验证(取公钥→用公钥验证jwt签名部分+token的颁发时间是否合规等)
- 从验证成功的id_token中提取出我们所需要的信息(email)
package main
import (
"context"
"crypto/rsa"
"encoding/json"
"fmt"
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"net/http"
"github.com/lestrrat-go/jwx/jwa"
"github.com/lestrrat-go/jwx/jwk"
)
// 接收json格式的body请求体用
type OAuthLoginRequest struct {
AccessToken string `form:"access_token" json:"access_token"`
IdToken string `form:"id_token" json:"id_token" binding:"required"`
}
// 写你的tenantID
var tenantID = "9e55725a-626e-4339-bcf7-2b3e915426fa"
// 验证token是否合法并且返回email.
// OAuthVerify
/* step1:从https://login.microsoftonline.com/[tenantID]/discovery/v2.0/keys中获取所有该tenantID的jwk公钥
step2从id_token中的第一部分中找到kid字段的values,该值定义了使用的是哪个公钥
step3:验证通过后,把id_token中的第二部分转换为map,然后提取出里面的email部分
step4:返回结果
*/
func (O *OAuthLoginRequest) OAuthVerify() (int, string, string) {
// step1
keySet, err := jwk.Fetch(context.Background(),
fmt.Sprintf("https://login.microsoftonline.com/%s/discovery/v2.0/keys", tenantID))
if err != nil {
return 10001, "failed to retrieve the public key from Microsoft", ""
}
// step2
token, err := jwt.Parse(O.IdToken, func(token *jwt.Token) (interface{}, error) {
// check Encryption method
if token.Method.Alg() != jwa.RS256.String() {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
// check if the path in keySet
kid, ok := token.Header["kid"].(string)
if !ok {
return nil, fmt.Errorf("kid header not found")
}
// check if the kid in keySet
keys, ok := keySet.LookupKeyID(kid)
if !ok {
return nil, fmt.Errorf("key %v not found", kid)
}
publicKey := &rsa.PublicKey{}
err = keys.Raw(publicKey)
if err != nil {
return nil, fmt.Errorf("could not parse pubkey")
}
return publicKey, nil
})
if err != nil {
return 10001, err.Error(), ""
}
if !token.Valid && func() bool {
if err = token.Claims.Valid(); err != nil {
return false
}
return true
}() {
return 10001, "Token verification failed.", ""
}
// step3
email, errMsg := GetEmailFromToken(token)
if errMsg != nil {
return 10001, errMsg.Error(), ""
}
// step4
return 0, "Token verification successes", email
}
// 控制器入口
func OauthLogin(c *gin.Context) {
var reqData OAuthLoginRequest
if err := c.ShouldBindJSON(&reqData); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"status": 7,
"msg": "The submitted form data is incorrect. Please verify and resubmit.", "data": map[string]any{}})
return
}
var email string
if err, msg, temp := reqData.OAuthVerify(); err != 0 {
c.JSON(http.StatusBadRequest, gin.H{"status": err, "msg": msg, "data": map[string]any{}})
return
} else {
email = temp
}
if err, msg, temp := reqData.OAuthVerify(); err != 0 {
c.JSON(http.StatusBadRequest, gin.H{"status": err, "msg": msg, "data": map[string]any{}})
return
} else {
email = temp
}
// fmt.Print(email)
// 当上面的email成功打印出来,证明id_token验证成功了,你可以直接发行自己账号系统的token了。
// TODO 绕过密码发行token
...
c.JSON(http.StatusBadRequest, gin.H{"status": http.StatusOK, "msg": email, "data": map[string]any{}})
return
}
// 从id_token中提取email
func GetEmailFromToken(token *jwt.Token) (string, error) {
// 1.claims to Json
claimsValue, err := json.Marshal(token.Claims)
if err != nil {
return "", fmt.Errorf("token parse failed(1)")
}
// 2.Json to map
var claimsMap map[string]interface{}
if jsonErr := json.Unmarshal(claimsValue, &claimsMap); jsonErr != nil {
return "", fmt.Errorf("token parse failed(2)")
}
// 3.Get email from map
email, ok := claimsMap["preferred_username"].(string)
if !ok {
return "", fmt.Errorf("token parse failed(3)")
}
return email, nil
}
func main() {
r := gin.Default()
r.POST("/Oauth/Login", OauthLogin)
r.Run(":8000")
}

结尾
没什么好说的,不建议用这种方式,最好还是用OAuth2.0的授权码流程验证。
参考:
1.USING AZURE AD AUTHENTICATION FOR A GO API
2.微软官方例子