基于Token的WEB后台认证机制(会话机制)
几种常用的认证机制
HTTP Basic Auth
HTTP Basic Auth简单点说明就是每次请求API时都提供用户的username和password,简言之,Basic Auth是配合RESTful API 使用的最简单的认证方式,只需提供用户名密码即可,但由于有把用户名密码暴露给第三方客户端的风险,在生产环境下被使用的越来越少。因此,在开发对外开放的RESTful API时,尽量避免采用HTTP Basic Auth
OAuth
OAuth(开放授权)是一个开放的授权标准,允许用户让第三方应用访问该用户在某一web服务上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。
OAuth允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的第三方系统(例如,视频编辑网站)在特定的时段(例如,接下来的2小时内)内访问特定的资源(例如仅仅是某一相册中的视频)。这样,OAuth让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容
下面是OAuth2.0的流程:
这种基于OAuth的认证机制适用于个人消费者类的互联网产品,如社交类APP等应用,但是不太适合拥有自有认证权限管理的企业应用;
Cookie Auth
Cookie认证机制就是为一次请求认证在服务端创建一个Session对象,同时在客户端的浏览器端创建了一个Cookie对象;通过客户端带上来Cookie对象来与服务器端的session对象匹配来实现状态管理的。默认的,当我们关闭浏览器的时候,cookie会被删除。但可以通过修改cookie 的expire time使cookie在一定时间内有效;
Token Auth
Token Auth的优点
Token机制相对于Cookie机制又有什么好处呢?
- 支持跨域访问: Cookie是不允许垮域访问的,这一点对Token机制是不存在的,前提是传输的用户认证信息通过HTTP头传输.
- 无状态(也称:服务端可扩展行):Token机制在服务端不需要存储session信息,因为Token 自身包含了所有登录用户的信息,只需要在客户端的cookie或本地介质存储状态信息.
- 更适用CDN: 可以通过内容分发网络请求你服务端的所有资料(如:javascript,HTML,图片等),而你的服务端只要提供API即可.
- 去耦: 不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在你的API被调用的时候,你可以进行Token生成调用即可.
- 更适用于移动应用: 当你的客户端是一个原生平台(iOS, Android,Windows 8等)时,Cookie是不被支持的(你需要通过Cookie容器进行处理),这时采用Token认证机制就会简单得多。
- CSRF:因为不再依赖于Cookie,所以你就不需要考虑对CSRF(跨站请求伪造)的防范。
- 性能: 一次网络往返时间(通过数据库查询session信息)总比做一次HMACSHA256计算 的Token验证和解析要费时得多.
- 不需要为登录页面做特殊处理: 如果你使用Protractor 做功能测试的时候,不再需要为登录页面做特殊处理.
- 基于标准化:你的API可以采用标准化的 JSON Web Token (JWT). 这个标准已经存在多个后端库(.NET, Ruby, Java,Python, PHP)和多家公司的支持(如:Firebase,Google, Microsoft).
基于JWT的Token认证机制实现
JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。其
JWT的组成
一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。
将头部进行Base64加密构成JWT token第一部分;
载荷(payload)进行base64加密构成JWT token第二部分 ;
将头部,荷载和签证拼在一起,用HMAC SHA256加密,得到的字符串为JWT token第三部分。
最后,将这三部分拼接在一起就是jwt,即: 头部. 载荷. 签名
头部(Header)
JWT还需要一个头部,头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象。
{
"typ": "JWT",
"alg": "HS256"
}
在头部指明了签名算法是HS256算法。
当然头部也要进行BASE64编码,编码后的字符串如下:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
载荷(Payload)
{ "iss": "Online JWT Builder",
"iat": 1416797419,
"exp": 1448333419,
"aud": "www.example.com",
"sub": "[email protected]",
"GivenName": "Johnny",
"Surname": "Rocket",
"Email": "[email protected]",
"Role": [ "Manager", "Project Administrator" ]
}
- iss: 该JWT的签发者,是否使用是可选的;
- sub: 该JWT所面向的用户,是否使用是可选的;
- aud: 接收该JWT的一方,是否使用是可选的;
- exp(expires): 什么时候过期,这里是一个Unix时间戳,是否使用是可选的;
- iat(issued at): 在什么时候签发的(UNIX时间),是否使用是可选的;
其他还有: - nbf (Not Before):如果当前时间在nbf里的时间之前,则Token不被接受;一般都会留一些余地,比如几分钟;,是否使用是可选的;
将上面的JSON对象进行[base64编码]可以得到下面的字符串。这个字符串我们将它称作JWT的Payload(载荷)。
eyJpc3MiOiJKb2huIFd1IEpXVCIsImlhdCI6MTQ0MTU5MzUwMiwiZXhwIjoxNDQxNTk0NzIyLCJhdWQiOiJ3d3cuZXhhbXBsZS5jb20iLCJzdWIiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiZnJvbV91c2VyIjoiQiIsInRhcmdldF91c2VyIjoiQSJ9
小知识:Base64是一种基于64个可打印字符来表示二进制数据的表示方法。由于2的6次方等于64,所以每6个比特为一个单元,对应某个可打印字符。三个字节有24个比特,对应于4个Base64单元,即3个字节需要用4个可打印字符来表示。JDK 中提供了非常方便的 BASE64Encoder 和 BASE64Decoder,用它们可以非常方便的完成基于 BASE64 的编码和解码
签名(Signature)
将上面的两个编码后的字符串都用句号.连接在一起(头部在前),就形成了:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0
最后,我们将上面拼接完的字符串用HS256算法进行加密。在加密的时候,我们还需要提供一个**(secret)。如果我们用mystar作为**的话,那么就可以得到我们加密后的内容:
rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM
最后将这一部分签名也拼接在被签名的字符串后面,我们就得到了完整的JWT:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0.rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM
那么,在我们输入账号密码通过数据库校验时,生成一个token,返回给前端,以后前端每次调用接口请求时,在报文头中添加key-value,那么URL中会带上这串JWT字符串:
前端每次调用接口请求时,在报文头中添加key-value:
Authorization:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0.rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM
认证过程
下面我们从一个实例来看如何运用JWT机制实现认证:
登录
- 第一次认证:第一次登录,用户从浏览器输入用户名/密码,提交后到服务器的登录处理的Action层(Login Action);
- Login Action调用认证服务进行用户名密码认证,如果认证通过,Login Action层调用用户信息服务获取用户信息(包括完整的用户信息及对应权限信息);
- 返回用户信息后,Login Action从配置文件中获取Token签名生成的秘钥信息,进行Token的生成;
- 生成Token的过程中可以调用第三方的JWT Lib生成签名后的JWT数据;
- 完成JWT数据签名后,将其设置到COOKIE对象中,并重定向到首页,完成登录过程;
请求认证
基于Token的认证机制会在每一次请求中都带上完成签名的Token信息,这个Token信息可能在COOKIE
中,也可能在HTTP的Authorization头中;
- 客户端(APP客户端或浏览器)通过GET或POST请求访问资源(页面或调用API);
- 认证服务作为一个Middleware HOOK 对请求进行拦截,首先在cookie中查找Token信息,如果没有找到,则在HTTP Authorization Head中查找;
- 如果找到Token信息,则根据配置文件中的签名加密秘钥,调用JWT Lib对Token信息进行解密和解码;
- 完成解码并验证签名通过后,对Token中的exp、nbf、aud等信息进行验证;
- 全部通过后,根据获取的用户的角色权限信息,进行对请求的资源的权限逻辑判断;
- 如果权限逻辑判断通过则通过Response对象返回;否则则返回HTTP 401;
对Token认证的五点认识
对Token认证机制有5点直接注意的地方:
- 一个Token就是一些信息的集合;
- 在Token中包含足够多的信息,以便在后续请求中减少查询数据库的几率;
- 服务端需要对cookie和HTTP Authrorization Header进行Token信息的检查;
- 基于上一点,你可以用一套token认证代码来面对浏览器类客户端和非浏览器类客户端;
- 因为token是被签名的,所以我们可以认为一个可以解码认证通过的token是由我们系统发放的,其中带的信息是合法有效的;
token只保存在客户端,却能在服务器端实现登入验证的原理:
如果用户解密出头部和荷载,更改了用户信息,服务器校验的时候用秘钥和客户端携带的头部和荷载通过HMAC SHA256加密得到JWT token的第三部分与用户携带的第三部分进行对比,不一样则认为是无效的JWT token,HMAC SHA256加密是不可逆的,因为用户不知道秘钥所以无法更改第三部分 。
JWT token只保存在客户端,跟session不同,可以减少服务器的压力。
JWT的JAVA实现的工具类
package com.utils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTCreationException;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.AccessToken;
import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class TokenUtil {
/**
* 加***
*/
private static final String SECRET = "432d2eb**************20dba3633";
/**
* 默认过期2小时
*/
private static final long EXPIRE_TIME = 2 * 60 * 60 * 1000;
private static final String JWT_ISSUER = "JWT";
private static final String ID_CLAIM = "id";
/**
* 生成token
*
* @param id 用户id
* @param expireTime 过期时间(毫秒ms)
* @return java.lang.String
*/
public static String sign(Long id, long expireTime) {
try {
Map<String, Object> headerClaims = new HashMap<String, Object>(2);
headerClaims.put("alg", "HS256");
headerClaims.put("typ", "JWT");
long currentTimeMillis = System.currentTimeMillis();
Date expireDate = new Date(currentTimeMillis + expireTime);
// 附带username信息
return JWT.create()
.withHeader(headerClaims)
.withIssuer(JWT_ISSUER)
.withClaim(ID_CLAIM, id)
.withIssuedAt(new Date(currentTimeMillis))
.withExpiresAt(expireDate)
.sign(Algorithm.HMAC256(SECRET));
} catch (UnsupportedEncodingException e) {
return null;
} catch (JWTCreationException exception) {
return null;
} catch (Exception e) {
return null;
}
}
/**
* 生成签名,60min后过期
*
* @param id 用户id
* @return java.lang.String
*/
public static String sign(Long id) {
try {
Map<String, Object> headerClaims = new HashMap<String, Object>(2);
headerClaims.put("alg", "HS256");
headerClaims.put("typ", "JWT");
long currentTimeMillis = System.currentTimeMillis();
Date expireDate = new Date(currentTimeMillis + EXPIRE_TIME);
// 附带username信息
return JWT.create()
.withHeader(headerClaims)
.withIssuer(JWT_ISSUER)
.withClaim(ID_CLAIM, id)
.withExpiresAt(expireDate)
.sign(Algorithm.HMAC256(SECRET));
} catch (UnsupportedEncodingException e) {
return null;
} catch (Exception e) {
return null;
}
}
/**
* 验证token
*
* @param token token值
* @return com.ucar.bean.AccessToken
*/
public static AccessToken verify(String token) {
AccessToken result = new AccessToken();
try {
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SECRET)).withIssuer(JWT_ISSUER).build();
DecodedJWT jwt = verifier.verify(token);
result.setVerify(Boolean.TRUE);
result.setId(jwt.getClaim(ID_CLAIM).asLong());
result.setSignDate(jwt.getIssuedAt());
result.setExpireDate(jwt.getExpiresAt());
result.setExpire(jwt.getExpiresAt().compareTo(new Date()) <= 0 ? true : false);
} catch (TokenExpiredException e) {
DecodedJWT jwt = JWT.decode(token);
result.setVerify(Boolean.TRUE);
result.setExpire(Boolean.TRUE);
result.setId(jwt.getClaim(ID_CLAIM).asLong());
result.setSignDate(jwt.getIssuedAt());
result.setExpireDate(jwt.getExpiresAt());
} catch (JWTVerificationException e) {
result.setVerify(Boolean.FALSE);
} catch (Exception e) {
result.setVerify(Boolean.FALSE);
}
return result;
}
/**
* 验证token
*
* @param token token值
* @return boolean
*/
public static boolean verifyToken(String token) {
try {
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SECRET))
.withIssuer(JWT_ISSUER)
.build();
verifier.verify(token);
return true;
} catch (JWTVerificationException e) {
return false;
} catch (Exception e) {
return false;
}
}
/**
* 校验token
*
* @param token token值
* @param id 用户的id
* @return boolean
*/
public static boolean verify(String token, Long id) {
try {
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SECRET))
.withIssuer(JWT_ISSUER)
.withClaim(ID_CLAIM, id)
.build();
verifier.verify(token);
return true;
} catch (JWTVerificationException e) {
return false;
} catch (Exception e) {
return false;
}
}
/**
* 获取用户id
*
* @param token token值
* @return java.lang.Long
*/
public static Long getId(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim(ID_CLAIM).asLong();
} catch (JWTDecodeException e) {
return null;
} catch (Exception e) {
return null;
}
}
/**
* 获取颁发时间
*
* @param token token值
* @return java.util.Date
*/
public static Date getIssuedDate(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getIssuedAt();
} catch (JWTDecodeException e) {
return null;
} catch (Exception e) {
return null;
}
}
/**
* 获取过期时间
*
* @param token token值
* @return java.util.Date
*/
public static Date getExpireDate(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getExpiresAt();
} catch (JWTDecodeException e) {
return null;
} catch (Exception e) {
return null;
}
}
/**
* 判断是否过期
*
* @param token token值
* @return boolean
*/
public static boolean isExpire(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getExpiresAt().compareTo(new Date()) <= 0 ? true : false;
} catch (JWTDecodeException e) {
return true;
} catch (Exception e) {
return true;
}
}
public static void main(String[] args) {
String token = sign(1L, 60 * 1000 * 24 * 365);
//String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJKRUVTRSIsImV4cCI6MTUyNjAxMjkyMywidXNlcklkIjoxLCJpYXQiOjE1MjYwMTI4NjN9.NKhWgl_L-TmZCOSOUzTaKQFYFfM7OrjG6O55BQ2Ts9M";
System.out.println(token);
AccessToken result = verify(token);
System.out.println(result.isVerify());
System.out.println(result.isExpire());
System.out.println(result.getSignDate());
System.out.println(result.getExpireDate());
System.out.println(result.getId());
System.out.println(verify(token, 1L));
System.out.println(getId(token));
}
}
然后,配置拦截器,实现每次请求的登入验证,默认全部请求进行拦截,然后通过自定义注解实现不需要登入验证。
package com.interceptor;
import com.alibaba.fastjson.JSONObject;
import com.AccessLogin;
import com.AccessToken;
import com.Constant;
import com.utils.TokenUtil;
import com.Result;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* 对所有的api请求进行拦截,验证请求头中是否携带合法且未过期的 token
*
* @author 吴佰川([email protected])创建
* @version 1.0
* @date 2018/10/26 8:36
*/
public class TokenInterceptor implements HandlerInterceptor {
private static final Logger LOGGER = LoggerFactory.getLogger(TokenInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if(handler instanceof HandlerMethod){
//检查是否有AccessLogin注释,有则跳过认证
AccessLogin accessLogin = ((HandlerMethod) handler).getMethodAnnotation(AccessLogin.class);
// 没有注释不验证
if (accessLogin != null && !accessLogin.required()) {
return true;
} else {
// 从 http 请求头中取出 token及uid
String token = request.getHeader(Constant.REQUEST_HEADER_TOKEN);
//token不存在
if (!StringUtils.isEmpty(token)) {
AccessToken accessToken = TokenUtil.verify(token);
if (accessToken.isVerify()) {
boolean isExpire = accessToken.isExpire();
if (isExpire) {
responseMsg(response, "token invalid");
return false;
}
request.setAttribute(Constant.REQUEST_HEADER_UID, accessToken.getId());
return true;
} else {
responseMsg(response, "token wrong");
return false;
}
} else {
responseMsg(response, "token does not exist");
return false;
}
}
} else {
return true;
}
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
/**
* 请求不通过,返回错误信息给客户端
*
* @param response 返回response
* @param msg 返回信息
* @return void
*/
private void responseMsg(HttpServletResponse response, String msg) throws IOException {
Result result = new Result();
result.setStatus(-3);
result.setMsg(msg);
response.setCharacterEncoding("utf-8");
response.setContentType("application/json; charset=utf-8");
String json = JSONObject.toJSONString(result);
PrintWriter out = response.getWriter();
out.print(json);
out.flush();
out.close();
}
}
package com.annotation;
import java.lang.annotation.*;
/**
* 登录自定义注解
*
*/
@Documented
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AccessLogin {
boolean required() default true;
}