背景:
在学习SpringCloud框架时,用到JWT做用户的认证登录和鉴权。为了更好的理解其工作原理,接下来就是具体学习JWT了。
什么是JWT?
JSON Web Token(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑且独立的方式,用于在各方之间作为JSON对象安全地传输信息。 此信息可以通过数字签名进行验证和信任。 JWT可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。
虽然JWT可以加密以在各方之间提供保密,但我们将专注于签名token。 签名token可以验证其中包含的声明的完整性,而加密token则隐藏其他方的声明。 当使用公钥/私钥对签名token时,签名还证明只有持有私钥的一方是签署它的一方。
何时使用JWT?
-
授权:这是我们使用JWT最广泛的应用场景。一次用户登录,后续请求将会包含JWT,对于那些合法的token,允许用户连接路由,服务和资源。如今在单独签名上是一种使用JWT的广泛特征。因为他们开销很小并且可以在不通领域轻松使用。
- 信息交换:JSON Web Token是一种在各方面之间安全信息传输的好的方式 因为JWT可以签名 - 例如,使用公钥/私钥对 - 您可以确定发件人是他们所说的人。 此外,由于使用标头和有效负载计算签名,您还可以验证内容是否未被篡改。
JSON Web Token由什么组成?
JSON Web Token由三部分组成,以dots " . " 分割。
他们是:
- Header
- Payload
- Signature
因此,一个JWT通常以下面这种形式出现。
xxxxx.yyyyy.zzzzz
分别看看不通部分。
Header
标头通常由两部分组成:token的类型,即JWT,以及正在使用的签名算法,例如HMAC SHA256或RSA。
For example:
{
"alg": "HS256",
"typ": "JWT"
}
然后,这个JSON被编码为Base64Url,形成JWT的第一部分。
Payload
token的第二部分是payload(有效负载),其中包含claims(声明)。Claims是关于一个实体(通常是用户)和其他数据类型的声名。claims有三种类型:registered,public,and private claims。
- 已注册的声明:这些是一组预定义声明,不是强制性的,但建议使用,以提供一组有用的,可互操作的声明。 其中一些是:iss(发行人),exp(到期时间),sub(主题),aud(观众)and others。(请注意,声明名称只有三个字符,因为JWT意味着紧凑。)
- 公开声明:这些可以由使用JWT的人随意定义。 但为避免冲突,应在IANA JSON Web Token Registry中定义它们,或者将其定义为包含防冲突命名空间的URI。
- 私人声明:这些声明是为了在同意使用它们的各方之间共享信息而创建的,并且既不是注册声明也不是公开声明。
An example payload could be:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
Signature(重点)
To create the signature part you have to take the encoded header, the encoded payload, a secret, the algorithm specified in the header, and sign that.
要创建签名部分,您必须采用编码header,编码的payload,a secret,标头中指定的算法,并对其进行签名。这段只读中文不好理解附上英文原版。
for example如果你想使用HMAC SHA256算法,签名会以下面的方式创建:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
签名用于验证消息在此过程中未被更改,并且,在使用私钥签名的token的情况下,它还可以验证JWT的发件人是否是它所声称的人。
Putting all together
输出是三个由点分隔的Base64-URL字符串,可以在HTML和HTTP环境中轻松传递,而与基于XML的标准(如SAML)相比更加紧凑。
下面显示了一个JWT,它具有先前的头和有效负载编码,并使用机密签名。 编码JWT
如果您想使用JWT并将这些概念付诸实践,您可以使用jwt.io Debugger来解码,验证和生成JWT。
How do JSON Web Tokens work?
在身份验证中,当用户使用其凭据成功登录时,将返回JSON Web令牌。 由于令牌是凭证,因此必须非常小心以防止出现安全问题。 一般情况下,您不应该将令牌保留的时间超过要求。
每当用户想要访问受保护的路由或资源时,用户代理应该使用承载模式发送JWT,通常在Authorization标头中。 标题的内容应如下所示:
Authorization: Bearer <token>
在某些情况下,这可以是无状态授权机制。 服务器的受保护路由将在Authorization标头中检查有效的JWT,如果存在,则允许用户访问受保护的资源。 如果JWT包含必要的数据,则可以减少查询数据库以进行某些操作的需要,尽管可能并非总是如此。
如果在Authorization标头中发送token,则跨域资源共享(CORS)将不会成为问题,因为它不使用cookie。
下图显示了如何获取JWT并用于访问API或资源:
- 应用程序或客户端向授权服务器请求授权。 这是通过其中一个不同的授权流程执行的。 例如,典型的OpenID Connect兼容Web应用程序将使用授权代码流通过/ oauth / authorize端点。
- 授予授权后,授权服务器会向应用程序返回访问token。
- 应用程序使用访问token来访问受保护资源(如API)。
请注意,使用签名token,token中包含的所有信息都会向用户或其他方公开,即使他们无法更改。 这意味着您不应该在token中放置秘密信息。
Why should we use JSON Web Tokens?
让我们来谈谈JSON Web Tokens(JWT)与Simple Web Tokens(SWT)和Security Assertion Markup Language Tokens(SAML)相比的好处。
由于JSON比XML更简洁,因此在编码时它的大小也更小,使得JWT比SAML更紧凑。这使得JWT成为在HTML和HTTP环境中传递的不错选择。
在安全方面,SWT只能使用HMAC算法通过共享密钥对称签名。但是,JWT和SAML token可以使用X.509证书形式的公钥/私钥对进行签名。与签名JSON的简单性相比,使用XML数字签名对XML进行签名而不会引入模糊的安全漏洞非常困难。
JSON解析器在大多数编程语言中很常见,因为它们直接映射到对象。相反,XML没有自然的文档到对象映射。这使得使用JWT比使用SAML断言更容易。
关于使用,JWT用于互联网规模。这突出了在多个平台(尤其是移动平台)上轻松进行JSON Web Token的客户端处理。
比较编码的JWT和编码的SAML的长度
如果您想了解有关JSON Web Tokens的更多信息,甚至开始使用它们在您自己的应用程序中执行身份验证,请浏览到Auth0上的JSON Web Token登录页面。
好了以上部分都是官方对JWT的介绍,下面来看看身份认证使用session、直接使用token以及JWT的过程
这三者间的比较呢
session对 多服务、移动端等的支持不好。
只用token的话解决了多服务、移动端之间通信的问题,且是一个独立的应用。但是增加了外部存储(redis)的依赖和代码实现相对复杂。
使用JWT的话代码简单,且没用session的问题,并且有自己的安全机制(多种加密方式等,通信安全不会被外部修改等优点)
最后来看看实际操作中Jwthelper工具类的编写,以及service中的调用
首先是Jwthelper工具类
package com.hzys.basicservice.user.utils;
import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.Map;
import org.apache.commons.lang3.time.DateUtils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.google.common.collect.Maps;
public class JwtHelper {
private static final String SECRET = "session_secret";
private static final String ISSUER = "mooc_user";
public static String genToken(Map<String, String> claims){
try {
//创建一个发布者为ISSUER和过期时间为一天的token
JWTCreator.Builder builder = JWT.create().withIssuer(ISSUER).withExpiresAt(DateUtils.addDays(new Date(), 1));
//将Map中的值存放入token中
claims.forEach((k,v) -> builder.withClaim(k, v));
//签名用的算法
Algorithm algorithm = Algorithm.HMAC256(SECRET);
//签名
return builder.sign(algorithm).toString();
} catch (IllegalArgumentException | UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
public static Map<String, String> verifyToken(String token) {
Algorithm algorithm = null;
try {
//签名算法
algorithm = Algorithm.HMAC256(SECRET);
} catch (IllegalArgumentException | UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
//根据签名和发布者创建JWT验证对象
JWTVerifier verifier = JWT.require(algorithm).withIssuer(ISSUER).build();
//验证token
DecodedJWT jwt = verifier.verify(token);
//获取token中存放的信息
Map<String, Claim> map = jwt.getClaims();
Map<String, String> resultMap = Maps.newHashMap();
//将token中信息放入新的HashMap中
map.forEach((k,v) -> resultMap.put(k, v.asString()));
return resultMap;
}
}
然后是service调用
package com.hzys.basicservice.user.service;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import com.hzys.basicservice.user.common.UserException;
import com.hzys.basicservice.user.mapper.UserMapper;
import com.hzys.basicservice.user.model.User;
import com.hzys.basicservice.user.utils.BeanHelper;
import com.hzys.basicservice.user.utils.HashUtils;
import com.hzys.basicservice.user.utils.JwtHelper;
import org.apache.commons.lang.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.alibaba.fastjson.JSON;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
@Service
public class UserService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private UserMapper userMapper;
@Autowired
private MailService mailService;
@Value("${file.prefix}")
private String imgPrefix;
/**
* 1.首先通过缓存获取
* 2.不存在将从通过数据库获取用户对象
* 3.将用户对象写入缓存,设置缓存时间5分钟
* 4.返回对象
*
* @param id
* @return
*/
public User getUserById(Long id) {
String key = "user:" + id;
String json = redisTemplate.opsForValue().get(key);
User user = null;
if (Strings.isNullOrEmpty(json)) {
user = userMapper.selectById(id);
user.setAvatar(imgPrefix + user.getAvatar());
String string = JSON.toJSONString(user);
redisTemplate.opsForValue().set(key, string);
redisTemplate.expire(key, 5, TimeUnit.MINUTES);
} else {
user = JSON.parseObject(json, User.class);
}
return user;
}
public List<User> getUserByQuery(User user) {
List<User> users = userMapper.select(user);
users.forEach(u -> {
u.setAvatar(imgPrefix + u.getAvatar());
});
return users;
}
/**
* 注册
*
* @param user
* @param enableUrl
* @return
*/
public boolean addAccount(User user, String enableUrl) {
user.setPasswd(HashUtils.encryPassword(user.getPasswd()));
BeanHelper.onInsert(user);
userMapper.insert(user);
registerNotify(user.getEmail(), enableUrl);
return true;
}
/**
* 发送注册激活邮件
*
* @param email
* @param enableUrl
*/
private void registerNotify(String email, String enableUrl) {
String randomKey = HashUtils.hashString(email) + RandomStringUtils.randomAlphabetic(10);
redisTemplate.opsForValue().set(randomKey, email);
redisTemplate.expire(randomKey, 1, TimeUnit.HOURS);
String content = enableUrl + "?key=" + randomKey;
mailService.sendSimpleMail("房产平台激活邮件", content, email);
}
public boolean enable(String key) {
String email = redisTemplate.opsForValue().get(key);
if (StringUtils.isBlank(email)) {
throw new UserException(UserException.Type.USER_NOT_FOUND, "无效的key");
}
User updateUser = new User();
updateUser.setEmail(email);
updateUser.setEnable(1);
userMapper.update(updateUser);
return true;
}
/**
* 校验用户名密码、生成token并返回用户对象
* @param email
* @param passwd
* @return
*/
public User auth(String email, String passwd) {
if (StringUtils.isBlank(email) || StringUtils.isBlank(passwd)) {
throw new UserException(UserException.Type.USER_AUTH_FAIL,"User Auth Fail");
}
User user = new User();
user.setEmail(email);
user.setPasswd(HashUtils.encryPassword(passwd));
user.setEnable(1);
List<User> list = getUserByQuery(user);
if (!list.isEmpty()) {
User retUser = list.get(0);
onLogin(retUser);
return retUser;
}
throw new UserException(UserException.Type.USER_AUTH_FAIL,"User Auth Fail");
}
private void onLogin(User user) {
String token = JwtHelper.genToken(ImmutableMap.of("email", user.getEmail(), "name", user.getName(),"ts",Instant.now().getEpochSecond()+""));
renewToken(token,user.getEmail());
user.setToken(token);
}
private String renewToken(String token, String email) {
redisTemplate.opsForValue().set(email, token);
redisTemplate.expire(email, 30, TimeUnit.MINUTES);
return token;
}
public User getLoginedUserByToken(String token) {
Map<String, String> map = null;
try {
map = JwtHelper.verifyToken(token);
} catch (Exception e) {
throw new UserException(UserException.Type.USER_NOT_LOGIN,"User not login");
}
String email = map.get("email");
Long expired = redisTemplate.getExpire(email);
if (expired > 0L) {
renewToken(token, email);
User user = getUserByEmail(email);
user.setToken(token);
return user;
}
throw new UserException(UserException.Type.USER_NOT_LOGIN,"user not login");
}
private User getUserByEmail(String email) {
User user = new User();
user.setEmail(email);
List<User> list = getUserByQuery(user);
if (!list.isEmpty()) {
return list.get(0);
}
throw new UserException(UserException.Type.USER_NOT_FOUND,"User not found for " + email);
}
public void invalidate(String token) {
Map<String, String> map = JwtHelper.verifyToken(token);
redisTemplate.delete(map.get("email"));
}
@Transactional(rollbackFor = Exception.class)
public User updateUser(User user) {
if (user.getEmail() == null) {
return null;
}
if (!Strings.isNullOrEmpty(user.getPasswd()) ) {
user.setPasswd(HashUtils.encryPassword(user.getPasswd()));
}
userMapper.update(user);
return userMapper.selectByEmail(user.getEmail());
}
public void resetNotify(String email,String url) {
String randomKey = "reset_" + RandomStringUtils.randomAlphabetic(10);
redisTemplate.opsForValue().set(randomKey, email);
redisTemplate.expire(randomKey, 1,TimeUnit.HOURS);
String content = url +"?key="+ randomKey;
mailService.sendSimpleMail("房产平台重置密码邮件", content, email);
}
public String getResetKeyEmail(String key) {
return redisTemplate.opsForValue().get(key);
}
public User reset(String key, String password) {
String email = getResetKeyEmail(key);
User updateUser = new User();
updateUser.setEmail(email);
updateUser.setPasswd(HashUtils.encryPassword(password));
userMapper.update(updateUser);
return getUserByEmail(email);
}
}
再加上user的model类
package com.hzys.basicservice.user.model;
import java.util.Date;
import org.springframework.web.multipart.MultipartFile;
import com.alibaba.fastjson.annotation.JSONField;
public class User {
private Long id;
private String name;
private String phone;
private String email;
private String aboutme;
private String passwd;
private String confirmPasswd;
private Integer type;
private Date createTime;
private Integer enable;
private String avatar;
@JSONField(deserialize=false,serialize=false)
private MultipartFile avatarFile;
private String newPassword;
private String key;
private Long agencyId;
private String token;
private String enableUrl;
private String agencyName;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getAboutme() {
return aboutme;
}
public void setAboutme(String aboutme) {
this.aboutme = aboutme;
}
public String getPasswd() {
return passwd;
}
public void setPasswd(String passwd) {
this.passwd = passwd;
}
public Integer getType() {
return type;
}
public void setType(Integer type) {
this.type = type;
}
public String getAgencyName() {
return agencyName;
}
public void setAgencyName(String agencyName) {
this.agencyName = agencyName;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
public String getConfirmPasswd() {
return confirmPasswd;
}
public void setConfirmPasswd(String confirmPasswd) {
this.confirmPasswd = confirmPasswd;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public Integer getEnable() {
return enable;
}
public void setEnable(Integer enable) {
this.enable = enable;
}
public MultipartFile getAvatarFile() {
return avatarFile;
}
public String getEnableUrl() {
return enableUrl;
}
public void setEnableUrl(String enableUrl) {
this.enableUrl = enableUrl;
}
public void setAvatarFile(MultipartFile avatarFile) {
this.avatarFile = avatarFile;
}
public String getNewPassword() {
return newPassword;
}
public String getAvatar() {
return avatar;
}
public void setAvatar(String avatar) {
this.avatar = avatar;
}
public void setNewPassword(String newPassword) {
this.newPassword = newPassword;
}
public Long getAgencyId() {
return agencyId;
}
public void setAgencyId(Long agencyId) {
this.agencyId = agencyId;
}
}
userMapper
package com.hzys.basicservice.user.mapper;
import java.util.List;
import com.hzys.basicservice.user.model.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper {
User selectById(Long id);
List<User> select(User user);
int update(User user);
int insert(User account);
int delete(String email);
User selectByEmail(String email);
}
最后是UserController
package com.hzys.basicservice.user.controller;
import com.hzys.basicservice.user.common.RestResponse;
import com.hzys.basicservice.user.model.User;
import com.hzys.basicservice.user.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("user")
public class UserController {
private static final Logger LOGGER = LoggerFactory.getLogger(UserController.class);
@Autowired
private UserService userService;
//-----------查询------------------
@RequestMapping("getById")
public RestResponse<User> getUserById(Long id) {
User user = userService.getUserById(id);
return RestResponse.success(user);
}
@RequestMapping("getByList")
public RestResponse<List<User>> getUserByList(@RequestBody User user){
List<User> list = userService.getUserByQuery(user);
return RestResponse.success(list);
}
/**
* 注册
* @param user
* @return
*/
@RequestMapping("add")
public RestResponse<User> add(@RequestBody User user) {
userService.addAccount(user, user.getEnableUrl());
return RestResponse.success();
}
/**
* 主要激活key的验证
*/
@RequestMapping("enable")
public RestResponse<Object> enable(String key){
userService.enable(key);
return RestResponse.success();
}
//------------------------登录/鉴权-------------------------
@RequestMapping("auth")
public RestResponse<User> auth(@RequestBody User user) {
User finalUser = userService.auth(user.getEmail(), user.getPasswd());
return RestResponse.success(finalUser);
}
@RequestMapping("get")
public RestResponse<User> getUser(String token){
User finalUser = userService.getLoginedUserByToken(token);
return RestResponse.success(finalUser);
}
@RequestMapping("logout")
public RestResponse<Object> logout(String token){
userService.invalidate(token);
return RestResponse.success();
}
}
以上实例来自慕课网学习视频如有兴趣可以前往慕课网学习观看。谢谢,如有不对之处,望不吝赐教!