常见的认证机制
http basic oauth
HTTP Basic Auth简单点说明就是每次
请求API时都提供用户的username和 password,
简言之,Basic Auth是配合RESTful API 使用的最简单
的认证方式,只需提供 用户名密码即可,但由于有把用户名密码暴露给第三方客户端的风险,在生产环境下被 使用的越来越少
。
因此,在开发对外开放的RESTful API时,尽量避免采用
HTTP Basic Auth
cookie oauth
OAuth2.0
token auth
防伪、业务信息、用户名、权限等都是存储在token中的
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认证机制实现
什么是JWT
JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用 户和服务器之间传递安全可靠的信息。
jwt由三个部分组成,其实就是一个字符串,三个部分组成,每个部分中间用 . 分隔
- 头部
公共数据的声明,例如token的类型,签名(安全,防伪)算法的计算
头部用于描述关于该JWT的最基本的信息
,例如其类型
以及签名所用的算法
等。这也可以 被表示成一个JSON
对象。
{"typ":"JWT","alg":"HS256"}
- 载荷
业务信息,用户名,用户权限等- 标准中注册的声明(建议但不强制使用)
iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。 - 其他声明(公共声明和私有声明)
可以添加任何信息,但不建议添加敏感信息,因为该部分在客户端可以解密。
- 标准中注册的声明(建议但不强制使用)
// 定义一个payload
{"sub":"1234567890","name":"John Doe","admin":true}
- 签名
防伪,保证生成的token是有效的,没有被篡改
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
- header(头部)
- payload(载荷)
- secret(加密使用的盐)
其中头部和载荷都是经过base64加密的,头部加载荷组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分
。
将这三部分用.连接成一个完整的字符串,构成了最终的jwt.
注意:
secret是保存在服务器端
的,jwt的签发生成也是在服务器端的,secret就是用 来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥
,在任何场景都不应该流 露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发
jwt了。
JJWT
JJWT是一个提供端到端的JWT创建和验证的Java库。永远免费和开源(Apache License,版本2.0),JJWT很容易使用和理解。它被设计成一个以建筑为中心的流畅界 面,隐藏了它的大部分复杂性。
依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.6.0</version>
</dependency>
创建及解析jwt
package test;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.junit.Test;
import java.util.*;
public class JjwtTest {
//使用jjwt创建jwt
@Test
public void test() {
//jwt不能再服务器端注销token 一旦token被其他人拿到,其他人就可以用我的身份操作
//一般情况,都会对jwt进行失效时间的设置
//获取当前时间
long now = System.currentTimeMillis();
//创建失效时间,一分钟后失效
Date exp = new Date(now + 60000l);
JwtBuilder jwtBuilder = Jwts.builder().setId("123456")//jti唯一标识符
.setIssuedAt(new Date())//iat:jwt签发时间
.claim("username", "zhangsan")//存放自定义业务信息
.claim("roles", "admin")
.setExpiration(exp)//设置失效时间
.signWith(SignatureAlgorithm.HS256, "itcast");//第一个参数是签名的算法,第二个参数是secret(盐)
//生成jwt
String jwt = jwtBuilder.compact();
System.out.println(jwt);
}
//解析jwt
@Test
public void test2() {
//String jwt = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMjM0NTYiLCJpYXQiOjE1NTUzMjAzMDl9.KCxQ8yTu76Gfnq8C5AXZWwsKZBc2mdujAtShL0i4KgE";
//String jwt = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMjM0NTYiLCJpYXQiOjE1NTUzMjA2MDcsInVzZXJuYW1lIjoiemhhbmdzYW4iLCJyb2xlcyI6ImFkbWluIn0.bNWruRo93J-X5xPEa9KYpiniNeIFp5biHb095p0GiAk";
String jwt = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMjM0NTYiLCJpYXQiOjE1NTUzMjA4MjUsInVzZXJuYW1lIjoiemhhbmdzYW4iLCJyb2xlcyI6ImFkbWluIiwiZXhwIjoxNTU1MzIwODg1fQ.kwMSiF6dH0tmKuNYpHMWp7notIjEwb1EtC1Jgg0gIWU";
Claims claims = Jwts.parser().setSigningKey("itcast").parseClaimsJws(jwt).getBody();
//https://note.youdao.com/ynoteshare1/index.html?id=d408c44d96f38ef0034872868d68189e&type=note#/
//Map<Integer, List> map = new HashMap<>();
//for (int i = 1; i < 10; i++) {
// map.put(i, new ArrayList());
//}
//map.get(3).add("");
//
//for (Map.Entry<Integer, List> entry : map.entrySet()) {
// if(entry.getKey()==...)
// for (Object o : entry.getValue()) {
//
// }
//}
System.out.println("jti:" + claims.getId());
System.out.println("iat:" + claims.getIssuedAt());
System.out.println("username:" + claims.get("username"));
System.out.println("roles:" + claims.get("roles"));
System.out.println("exp:" + claims.getExpiration());
}
}
jjwt工具类
package util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* Created by Administrator on 2018/4/11.
*/
@ConfigurationProperties("jwt.config")
public class JwtUtil {
private String key ;
private long ttl ;//一个小时
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public long getTtl() {
return ttl;
}
public void setTtl(long ttl) {
this.ttl = ttl;
}
/**
* 生成JWT
*
* @param id 唯一标识符
* @param subject 用户信息
* @param 用户角色
* @return
*/
public String createJWT(String id, String subject, String roles) {
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
JwtBuilder builder = Jwts.builder().setId(id)
.setSubject(subject)
.setIssuedAt(now)
.signWith(SignatureAlgorithm.HS256, key).claim("roles", roles);
if (ttl > 0) {
builder.setExpiration( new Date( nowMillis + ttl));
}
return builder.compact();
}
/**
* 解析JWT
* @param jwtStr
* @return
*/
public Claims parseJWT(String jwtStr){
return Jwts.parser()
.setSigningKey(key)
.parseClaimsJws(jwtStr)
.getBody();
}
}
springmvc拦截器将进行token传递
在拦截之后通过request将token传递给controller层,具体实现代码是下面那个
package com.tensquare.user.intercept;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class MyIntercept implements HandlerInterceptor {
//之前,进入Controller方法之前执行
//返回false表示拦截,不会再执行后面的Controller方法
//返回true表示放行,继续执行后面的Controller方法
//使用场景:登录拦截,权限处理
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("拦截器的preHandle");
return true;
}
//之中,进入Controller,执行方法逻辑,但是在返回ModelAndView结果之前执行
//使用场景: 存放公共数据,例如统一存放双十一活动数据
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("拦截器的postHandle");
}
//之后,进入Controller,执行方法逻辑,返回ModelAndView结果之后,最后执行
//使用场景: 日志记录 ,异常处理
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("拦截器的afterCompletion");
}
}
拦截器开放登录路径
不能所有方法都调用 jwt
来验证一遍权限
package com.tensquare.user.config;
import com.tensquare.user.intercept.MyIntercept;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
@Autowired
private MyIntercept myIntercept;
@Override
protected void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(myIntercept)
.addPathPatterns("/**")//设置哪些路径被拦截器拦截,在进入请求路径之前,拦截器的三个方法都会走一遍
.excludePathPatterns("/**/login");//登录请求不被拦截
}
}
拦截器在进入controller前获取jwt token
@Component
public class JwtFilter extends HandlerInterceptorAdapter
{
@Autowired private JwtUtil jwtUtil;
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
{
System.out.println("经过了拦截器");
final String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer "))
{
final String token = authHeader.substring(7); // The part after "Bearer "
Claims claims = jwtUtil.parseJWT(token);
if (claims != null)
{
if("admin".equals(claims.get("roles")))
{//如果是管理员
// 这个方法是将属性设置到request中,以便controller层获取,还有另一种方式是通过ThreadLocal在下面
request.setAttribute("admin_claims", claims); }
if("user".equals(claims.get("roles"))){//如果是用户
request.setAttribute("user_claims", claims); } } }
return true; } }
controller获取token
/*** 删除
* @param id
*/
@RequestMapping(value="/{id}",method= RequestMethod.DELETE)
public Result delete(@PathVariable String id ){
Claims claims=(Claims) request.getAttribute("admin_claims");
if(claims==null)
{
return new Result(true,StatusCode.ACCESSRROR,"无权访问");
}
userService.deleteById(id);
return new Result(true,StatusCode.OK,"删除成功"); }
通过ThreadLocal传递token
package com.tensquare.user.util;
import io.jsonwebtoken.Claims;
public class ThreadLocalUtil {
private static ThreadLocal<Claims> threadLocal = new ThreadLocal<>();
public static void set(Claims claims) {
threadLocal.set(claims);
}
public static Claims get() {
Claims claims = threadLocal.get();
return claims;
}
public static void clean() {
threadLocal.remove();
}
}
@Component
public class MyIntercept implements HandlerInterceptor {
@Autowired
private JwtUtil jwtUtil;
//之前,进入Controller方法之前执行
//返回false表示拦截,不会再执行后面的Controller方法
//返回true表示放心,继续执行后面的Controller方法
//使用场景:登录拦截,权限处理
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//System.out.println("拦截器的preHandle");
//拦截器只是对token进行解析,没有进行相关的业务处理,因为业务是各种各样
//获取到token,从请求头Authorization中获取数据
String header = request.getHeader("Authorization");
//判断获取到的数据是否为空
if (header == null || "".equals(header)) {
//如果为空表示没有token
//不用传递claims,直接返回true,放行即可
return true;
}
//判断数据是否Bearer 开头
if (!header.startsWith("Bearer ")) {
//如果不是要求的数据开头,token不合法
//不用传递claims,直接返回true,放行即可
return true;
}
try {
//截取token内容,从7开始的字符串
String token = header.substring(7);
//使用jwt工具类,对token进行解析,获取claims
Claims claims = jwtUtil.parseJWT(token);
//具体的业务是多种多样的,可能需要用户id,用户名字,用户权限...
//没有必要一定在拦截器中进行处理,直接返回claims即可
//把claims传递给Controller方法
//方法一:使用request把claims传递给Controller
//request.setAttribute("claims", claims);
//方法二:可以把claims放到线程中,进行传递,ThreadLocal
ThreadLocalUtil.set(claims);
} catch (Exception e) {
e.printStackTrace();
}
//例如查看文章不需要身份认证,所全部放行,返回true
return true;
}
乱世之中,带兵的人说话就是王法。
饿时吃糠甜如蜜,饱时吃蜜都不甜。
鬼吹灯
天下霸唱