JWT
JWT全称“JSON Web Token”,是基于JSON的用户身份认证的令牌。可跨域身份认证,所以JWT很适合做分布式的鉴权,单点登录(SingleSign,SSO)。
jwt有三部分组成用符号"."隔开,
HEADER:token头,描述token是什么类型,加密方式是什么,把json内容转为base64
PAYLOAD:内容,是暴露出来的信息,不可存敏感信息,把json内容转为base64
SIGNATURE:签名,按token头的加密方式把HEADER和PAYLOAD的信息加密生成签名,下面是官网上面的介绍,地址:https://jwt.io/
jwt的token是不可以篡改的,虽然前两部分的内容可以base64解码之后就能看到明文,但由于第三部分签名是把前两部分内容用一个密钥加密的,验证的时候也是把前两部分内容再次加密和原来签名对比是否一致,若内容被篡改了,则两次签名不一致校验不通过。
SSO
问题一:同一个公司的系统 ,不如果每个系统都有一套自己的用户名密码,那用户记
得头都大了啊。所以这时产生了一个鉴权中心,全部系统用同一套用户信息,同一个地方登录。
问题二:用同一套用户信息可以了,但如果进每个系统都要输一次账户密码登录还是很麻烦的。所以这里还要一处登录 ,处处登录,登录了其中一个系统,进入其它系统的时候不需要登录
效果如下图所示
用户在sso中心登录后的token在站点A,站点B都能使用,并且站点A,站点B和sso中心不需要通讯也能自己鉴别token是否是有效的。
怎么做到站点和sso串联呢?具体的流程是用户打开站点A,发现未登录 ,那站点A会跳转到sso中心登录并且把自己的url带上,sso中心登录成功后,跳转回站点带过来的url并把token也带上。
那站点A登录成功了,站点B怎么共享这个Token呢,做法是,sso中心登录成功的时候同时存一份Token到cookie(或localstorage等地方),当用户进入站点B的时候,发现没登录,跳转到sso中心带上自己的url,sso中心发现cookie有token了,直接跳转回站点B的url并把token带上,这样站点B就能实现token共享和自动登录了。
代码实现
鉴权中心核心代码
新建一个AuthenticationCenter项目
新建一个AuthenticatinController控制器
/// <summary>
/// 鉴权相关
/// </summary>
public class AuthenticationController : Controller
{
/// <summary>
/// 登录界面
/// </summary>
/// <param name="redirectUrl">登录成功后跳转页面</param>
/// <returns></returns>
[HttpGet]
public IActionResult Login(string redirectUrl)
{
//取出登录token
string token = string.Empty;
//从cookie把token取出来
Request.Cookies.TryGetValue("identity_token", out token);
//实际还需验证token是否有效
if(!string.IsNullOrEmpty(token))
{
//已经登录过,直接跳转回网站
redirectUrl += $"?token={token}";
return Redirect(redirectUrl);
}
ViewBag.redirectUrl = redirectUrl;
//没登录过,展示登录界面
return View();
}
[HttpPost]
public IActionResult Login(string account, string password, string redirectUrl)
{
if (account == "admin" && password == "123456") //实际数据库校验
{
User user = new User()
{
userId = 1000,
account = account,
userName = "张三",
age = 18,
email = "zhangsan@qq.com"
};
string token = new JWTService().GetJwtToken(user);
//把token写入cookie
Response.Cookies.Append("identity_token", token);
//登录成功跳转到对应系统
redirectUrl+= $"?token={token}";
return Redirect(redirectUrl);
}
else
{
//登录失败
return Content("登录失败");
}
}
/// <summary>
/// 退出登录
/// </summary>
/// <returns></returns>
[HttpGet]
public IActionResult LogOut()
{
//退出登录
HttpContext.Response.Cookies.Delete("identity_token");
//回到登录页面
return RedirectToAction("Login");
}
}
Login的view视图
<form action="/Authentication/Login" method="post">
<input type="hidden" name="redirectUrl" value="@ViewBag.redirectUrl"/>
账号: <input name="account"/><br />
密码:<input name="password" /><br />
<input type="submit" value="登录" />
</form>
其它相关类
/// <summary>
/// jwt处理
/// </summary>
public class JWTService
{
public string GetJwtToken(User user)
{
var claims = new[]
{
new Claim(ClaimTypes.Name, user.userId.ToString()),
new Claim("userName", user.userName),
new Claim("account", user.account),
new Claim("age", user.age.ToString()),
new Claim("email", user.email)
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(ConstOptions.SecurityKey));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
/**
* Claims (Payload)
Claims 部分包含了一些跟这个 token 有关的重要信息。 JWT 标准规定了一些字段,下面节选一些字段:
iss: The issuer of the token,token 是给谁的
sub: The subject of the token,token 主题
exp: Expiration Time。 token 过期时间,Unix 时间戳格式
iat: Issued At。 token 创建时间, Unix 时间戳格式
jti: JWT ID。针对当前 token 的唯一标识
除了规定的字段外,可以包含其他任何 JSON 兼容的字段。
*/
var token = new JwtSecurityToken(
issuer: ConstOptions.Issuer,
audience: ConstOptions.Audience,
claims: claims,
expires: DateTime.Now.AddMinutes(60),//有效期
notBefore: DateTime.Now,//开始有效时间,可以往后设置
signingCredentials: creds);
string returnToken = new JwtSecurityTokenHandler().WriteToken(token);
return returnToken;
}
}
/// <summary>
/// 常量
/// </summary>
public static class ConstOptions
{
//密钥
public const string SecurityKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDI2a2EJ7m872v0afyoSDJT2o";
public const string Issuer = "jwtIssuer"; //发送者
public const string Audience = "jwtAudience";//签收者
public const string identityToken = "identity_token";
}
public class User
{
public int userId { get; set; }
public string account { get; set; }
public string userName { get; set; }
public int age { get; set; }
public string email { get; set; }
}
上面sso的登录功能就完成了,打开Login页面就能获取到Token了。
站点核心代码
新建一个站点A
修改startup.cs文件,在ConfigureServices方法里加上
//jwt校验对称加密
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => {
options.TokenValidationParameters = new TokenValidationParameters
{
//Audience,Issuer,SecurityKey的值要和sso的一致
//JWT有一些默认的属性,就是给鉴权时就可以筛选了
ValidateIssuer = true,//是否验证Issuer
ValidateAudience = true,//是否验证Audience
ValidateLifetime = true,//是否验证失效时间
ValidateIssuerSigningKey = true,//是否验证SecurityKey
ValidAudience = ConstOptions.Audience,//
ValidIssuer = ConstOptions.Issuer,//Issuer,这两项和前面签发jwt的设置一致
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(ConstOptions.SecurityKey)),//拿到SecurityKey
};
});
在Configure方法里加上
app.UseAuthentication();//鉴权:解析信息--就是读取token,解密token
新建一个UserController
public class UserController : Controller
{
/// <summary>
/// 登录
/// </summary>
/// <returns></returns>
[AllowAnonymous]
public IActionResult Login()
{
//重定向到 sso登录
return Redirect("http://localhost:5000/Authentication/Login?redirectUrl=http://localhost:27271/User/LoginSuccess");
}
[AllowAnonymous]
public string LoginSuccess(string token)
{
return $"登录成功,token:{token}";
}
[Authorize]
public IActionResult GetString()
{
return Content("ok");
}
[Authorize]
public IActionResult GetName()
{
var str = string.Empty;
var Claims = HttpContext.User.Identities.First().Claims;
var name = Claims.Where(s => s.Type == "userName").First().Value;
foreach (var item in Claims)
{
str += $"{item.Type}:{item.Value},";
}
return Content(str);
}
}
其它相关类
public class ConstOptions
{
public const string SecurityKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDI2a2EJ7m872v0afyoSDJT2o";
public const string Issuer = "jwtIssuer";
public const string Audience = "jwtAudience";
}
密钥要和sso中心的保持一致,上面的5000端口是sso的端口,27271端口是站点端口。
一开始我输入的是端口为27271站点A的User/Login,然后直接跳转到端口5000的sso中心,然后登录完又回到27271的登录成功界面,第二次我再在地址上输入端口为27271站点A的User/Login,然后不需要输入密码就回到自己的登录成功页面上了,两次的Token一致,如果把A的代码复制一份变成站点B,也一样是不用登录的。
验证Token是否有效,获取用户信息
借助postman,站点A的/User/GetString方法头部加上 [Authorize]特性后表明要校验身份,这时候header没传token,会报401没鉴权把刚才登录成功的token放到header里面的Authorization请求,就能看到成功返回ok,注意Token前面要加上Bearer和一个空格,这个是标准格式。
站点A如果读取Token的信息呢,上面说了Token的内容是中间的payload,只是用了base64编码了,直接截取base64解码就能拿到了,或者像上面的/User/GetName里面获取。