前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Token认证

Token认证

作者头像
爱撒谎的男孩
发布2019-12-31 15:44:40
2K0
发布2019-12-31 15:44:40
举报
文章被收录于专栏:码猿技术专栏码猿技术专栏

文章目录

1. 认证机制

1.1. 常见的几种认证机制

1.1.1. HTTP Basic Auth

1.1.2. OAuth(开放授权)

1.1.3. Cookie/Session 认证机制

1.1.4. 基于 Token 的认证机制

1.1.5. 有状态服务和无状态服务

1.2. 基于JWT(JSON WEB TOKEN)的Token认证机制实现

1.2.1. 头部(Header)

1.2.2. 载荷(Payload)

1.2.3. 签名(Signature)

1.3. JJWT

1.3.1. 添加依赖

1.3.2. 生成token

1.3.3. 解析token

1.3.4. 设置过期时间

1.3.5. 添加自定义属性

1.4. 在拦截器中配置

1.4.1. JWT工具类

1.4.2. 配置文件

1.4.3. 拦截器

1.4.4. 配置拦截器

1.4.5. 使用

1.5. 相关问题

1.6. 开发流程

1.7. 源码

1.8. 参考文章

认证机制

常见的几种认证机制

HTTP Basic Auth

  • 在HTTP中,HTTP基本认证是一种允许Web浏览器或者其他客户端在请求时提供用户名和口令形式的身份凭证的一种登录验证方式。
  • 简单而言,HTTP基本认证就是我们平时在网站中最常用的通过用户名和密码登录来认证的机制。 就是每次请求都会带上用户名和密码
  • 优点
    • HTTP 基本认证是基本上所有流行的网页浏览器都支持。但是基本认证很少在可公开访问的互联网网站上使用,有时候会在小的私有系统中使用。
    • 适用于各种平台,包括app和web
  • 缺点
    • HTTP 基本认证虽然足够简单,但是前提是在客户端和服务器主机之间的连接足够安全。如果没有使用SSL/TLS这样的传输层安全的协议,那么以明文传输的密钥和口令很容易被拦截。
    • 由于现存的浏览器保存认证信息直到标签页或浏览器关闭,或者用户清除历史记录。导致了服务器端无法主动来当前用户登出或者认证失效。

OAuth(开放授权)

  • OAuth 是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表等),而无需将用户名和密码提供给第三方应用。
  • OAuth 允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的网站(例如,视频编辑网站)在特定的时段(例如,接下来的2小时内)内访问特定的资源(例如仅仅是某一相册中的视频)。这样,OAuth让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容。
  • 最常见的就是qq和微信授权登录

Cookie/Session 认证机制

  • Cookie 是由客户端保存的小型文本文件,其内容为一系列的键值对。Cookie 是由 HTTP 服务器设置的,保存在浏览器中。Cookie会随着 HTTP请求一起发送。
  • Session 是存储在服务器端的,避免在客户端 Cookie 中存储敏感数据。Session 可以存储在 HTTP 服务器的内存中,也可以存在内存数据库(如redis)中。
  • Cookie/Session认证机制就是为一次请求认证在服务端创建一个Session对象,同时在客户端的浏览器端创建了一个Cookie对象;通过客户端带上来Cookie对象来与服务器端的session对象匹配来实现状态管理的。默认的,当我们关闭浏览器的时候,cookie会被删除。但可以通过修改cookie 的expire time使cookie在一定时间内有效
  • 缺点:
    • 平台有限,不适合App端
    • 数据量过大的话,对服务器会造成负担

基于 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).

有状态服务和无状态服务

  • 无状态服务:就是没有特殊状态的服务,各个请求对于服务器来说统一无差别处理,请求自身携带了所有服务端所需要的所有参数(服务端自身不存储跟请求相关的任何数据,不包括数据库存储信息)
  • 有状态服务:与之相反,有状态服务在服务端保留之前请求的信息,用以处理当前请求,比如session

基于JWT(JSON WEB TOKEN)的Token认证机制实现

  • 一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。

头部(Header)

  • JWT还需要一个头部,头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象。
代码语言:javascript
复制
{
    "typ": "JWT",
    "alg": "HS256"
}

载荷(Payload)

  • iss: 该JWT的签发者,是否使用是可选的;
  • sub: 该JWT所面向的用户,一般是用户名,是否使用是可选的;
  • aud: 接收该JWT的一方,是否使用是可选的;
  • exp(expires): 什么时候过期,这里是一个Unix时间戳,是否使用是可选的;
  • iat(issued at): 在什么时候签发的(UNIX时间),一般是登录时间,是否使用是可选的; 其他还有:
  • nbf (Not Before):如果当前时间在nbf里的时间之前,则Token不被接受;一般都会留一些余地,比如几分钟;,是否使用是可选的;
代码语言:javascript
复制
{ "iss": "Online JWT Builder", 
  "iat": 1416797419, 
  "exp": 1448333419, 
  "aud": "www.example.com", 
  "sub": "jrocket@example.com", 
  "GivenName": "Johnny", 
  "Surname": "Rocket", 
  "Email": "jrocket@example.com", 
  "Role": [ "Manager", "Project Administrator" ] 
}
  • 将上面的JSON对象进行[base64编码]可以得到编码后的字符串。这个字符串我们将它称作JWT的Payload(载荷)。

签名(Signature)

  • 将头部和载荷编码后的字符串用.分隔(头部在前),最后将拼接后的字符串和秘钥(secret)用头部指定的算法进行加密得到一个字符串。那么此时完整的JWT的内容就是头部+载荷+最后加密得到的字符串,中间用.分割

JJWT

  • Java实现JWT的token生成

添加依赖

代码语言:javascript
复制
<!-- jjwt -->
<dependency>
	<groupId>io.jsonwebtoken</groupId>
	<artifactId>jjwt</artifactId>
	<version>0.9.0</version>
</dependency>

生成token

代码语言:javascript
复制
/**
 *  	生成token
 */
@Test
public void test1() {
	//eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxIiwiaWF0IjoxNTQ1NTc0ODE1LCJzdWIiOiLpmYjliqDlhbUifQ.WF0VoGSP5oH0XRsCraJ9lRjtVFRs6I0KJpkhFngpwgk
	JwtBuilder builder = Jwts.builder()
			.setId("1")   //用户Id
               .setIssuedAt(new Date())  //用户登录的日期 
               .setSubject("陈加兵")//用户名
               .signWith(SignatureAlgorithm.HS256, "sercet");  //指定签名的算法和秘钥(盐)  
	String token = builder.compact();  //获取生成的token
	System.out.println(token);
}

解析token

  • 解析token需要知道秘钥
代码语言:javascript
复制
/*
	 * 	解析token
	 */
	@Test
	public void test2() {
		Claims claims = Jwts.parser()
				.setSigningKey("sercet")  //设置解析的秘钥
				.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxIiwiaWF0IjoxNTQ1NTc0ODE1LCJzdWIiOiLpmYjliqDlhbUifQ.WF0VoGSP5oH0XRsCraJ9lRjtVFRs6I0KJpkhFngpwgk")
				.getBody();
		
		System.out.println("用户Id:"+claims.getId());
		System.out.println("用户名:"+claims.getSubject());
		System.out.println("登录时间:"+claims.getIssuedAt());
		
		
	}

设置过期时间

  • 不设置过期时间默认是无时间限制的
  • JwtBuilder setExpiration(Date exp);
代码语言:javascript
复制
JwtBuilder builder = Jwts.builder()
				.setId("1")   //用户Id
                .setIssuedAt(new Date())  //用户登录的日期 
                .setSubject("陈加兵")//用户名
                .setExpiration(new Date(new Date().getTime()+1000*60*60))  //设置过期时间为1小时
                .signWith(SignatureAlgorithm.HS256, "sercet");  //指定签名的算法和秘钥(盐)

添加自定义属性

  • JwtBuilder claim(String name, Object value);: 直接添加自定义的属性,key-value形式
  • JwtBuilder addClaims(Map<String, Object> claims);: 直接添加一个Map作为自定义的属性
代码语言:javascript
复制
/**
	 *  	生成token
	 */
	@Test
	public void test1() {
		JwtBuilder builder = Jwts.builder()
				.setId("1")   //用户Id
                .setIssuedAt(new Date())  //用户登录的日期 
                .setSubject("陈加兵")//用户名
                .setExpiration(new Date(new Date().getTime()+1000*60*60))  //设置过期时间为1小时
                .signWith(SignatureAlgorithm.HS256, "sercet") //指定签名的算法和秘钥(盐)
                .claim("age", 22)    //自定义内容
                .claim("address", "江苏省"); //自定义内容
		String token = builder.compact();  //获取生成的token
		System.out.println(token);
	}
	
	
	/*
	 * 	解析token
	 */
	@Test
	public void test2() {
		Claims claims = Jwts.parser()
				.setSigningKey("sercet")  //设置解析的秘钥
				.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxIiwiaWF0IjoxNTQ1NTc1ODQzLCJzdWIiOiLpmYjliqDlhbUiLCJleHAiOjE1NDU1Nzk0NDMsImFnZSI6MjIsImFkZHJlc3MiOiLmsZ_oi4_nnIEifQ.uRhzSnsWl5IO-K6SA3zHsqGacZzkOOsFlD8lvqYDleY")
				.getBody();
		
		System.out.println("用户Id:"+claims.getId());
		System.out.println("用户名:"+claims.getSubject());
		System.out.println("登录时间:"+claims.getIssuedAt());
		System.out.println("过期时间:"+claims.getExpiration());
		System.out.println("address:"+claims.get("address"));
	}

在拦截器中配置

JWT工具类

代码语言:javascript
复制
import java.util.Date;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Data;
/**
 * JWT的工具类
 */
@Component
@ConfigurationProperties(prefix="jwt.config")  //读取配置文件中的配置
@Data
public class JwtUtil {
	private String secret;  //秘钥
	private long expire;  //过期时间
	
	/**
	 * 	生成token 
	 * @param id  用户的Id
	 * @param subject  用户名
	 * @param role 角色,分为用户和后台管理员
	 * @return
	 */
	public String encoder(String id,String subject,String role) throws Exception{
		JwtBuilder builder = Jwts.builder()
				.setId(id)   //用户Id
                .setIssuedAt(new Date())  //用户登录的日期 
                .setSubject(subject)//用户名
                .setExpiration(new Date(new Date().getTime()+expire))  //设置过期时间为1小时
                .claim("role",role)  //自定义属性,指定角色
                .signWith(SignatureAlgorithm.HS256, secret); //指定签名的算法和秘钥(盐)
                
		return builder.compact();  
	}
	
	/**
	 * 	对token进行解码
	 * @param token
	 * @return 解码后的结果集,相当于Map
	 * @throws Exception  如果解码失败会抛出异常
	 */
	public Claims decoder(String token) throws Exception{
		return Jwts.parser()
				.setSigningKey(secret)  //设置解析的秘钥
				.parseClaimsJws(token)  //解析的token
				.getBody();
	}
}

配置文件

代码语言:javascript
复制
jwt: # JWT的配置
  config:
    secret: secret   ## 秘钥
    expire: 3600000  ## 过期时间1个小时

拦截器

代码语言:javascript
复制
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import cn.tedu.auth.util.JwtUtil;
import io.jsonwebtoken.Claims;

/**
 * JWT验证token的拦截器
 * 	改进: 如果没有权限,那么可以跳转到一个指定的错误页面护.......
 */
public class JwtInterceptor extends HandlerInterceptorAdapter {
	
	@Resource
	private JwtUtil jwtUtil;    //注入JwtUtil
	
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		
		//获取请求头中的token
		String token=request.getHeader("token");
		if (StringUtils.isEmpty(token)) {
			response.setStatus(HttpStatus.UNAUTHORIZED.value());  //设置401响应信息,没有权限
			System.err.println("没有权限");
			return false;  //直接拦截,不继续进行
		}
		
		//如果有token,需要解码
		Claims claims=null;
		try {
			System.err.println(token);
			claims = jwtUtil.decoder(token);
			if (claims!=null) {
				request.setAttribute("claims", claims);  //放置在request中,后续的接口可能还需使用
			}
		} catch (Exception e) {
			response.setStatus(HttpStatus.UNAUTHORIZED.value());  //设置401响应信息,没有权限
			return false; 
		}
		
		return true;
	}
}

配置拦截器

  • 一定要先注入拦截器类,否则拦截器内的其他对象将不能注入
代码语言:javascript
复制
@Configuration
public class webConfig extends WebMvcConfigurerAdapter {

	/**
	 * 注入拦截器,这里一定需要提前注入,否则拦截器中注入的对象将无法注入
	 * 
	 * @return
	 */
	@Bean
	public JwtInterceptor jwtInterceptor() {
		return new JwtInterceptor();
	}

	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		// 注册自定义拦截器,添加拦截路径和排除拦截路径 ,这里直接使用上面的方法直接获取注入的拦截器即可,否则将会造成拦截器中无法注入其他的对象
		registry.addInterceptor(jwtInterceptor()).addPathPatterns("/**").excludePathPatterns("/user/test")
				.excludePathPatterns("/user/login");
	}
}

使用

  • 登录成功返回token
代码语言:javascript
复制
public ResultInfo login(User user)throws Exception{
		ResultInfo resultInfo=new ResultInfo();
		User user2 = userRepository.findByName(user.getName());  
		if (user2==null) {
			resultInfo.setCode("-1");
			resultInfo.setMessage("用户名不存在");
			return resultInfo;
		}
		
		//判断密码是否正确
		if (!bCryptPasswordEncoder.matches(user.getPassword(),user2.getPassword())) {
			resultInfo.setCode("-1");
			resultInfo.setMessage("密码不正确");
			return resultInfo;
		}
		
		//生成token
		String token=JwtUtil.encoder(user2.getId()+"", user2.getName(),"user");
		System.err.println(token);
		
		Map<String, Object> map=new HashMap<>();
		map.put("token", token);   //返回token
		map.put("user", user);
		resultInfo.setData(map);
		resultInfo.setMessage("登录成功");
		return resultInfo;
	}
  • 删除操作需要验证token是否具有指定的权限
    • 拦截器验证
    • controller中的角色验证码(后期可以使用切面将其提取出来)
代码语言:javascript
复制
/**
 *   删除用户
 * @param id
 * @return
 */
@DeleteMapping("/{id}")
public ResultInfo deleteById(@PathVariable("id")Integer id,HttpServletRequest request) {
	ResultInfo resultInfo=new ResultInfo();
	
	//验证角色,之后该段可以直接用切面完成
	Claims claims = (Claims) request.getAttribute("claims"); //获取token解析的map
	String role=(String) claims.get("role");  //获取角色
	if (!"user".equals(role)) {
		resultInfo.setCode("-1");
		resultInfo.setMessage("权限不足!");
		return resultInfo;
	}
	
	try {
		resultInfo=userService.deleteById(id);
		return resultInfo;
	} catch (Exception e) {
		resultInfo.setCode("-1");
		resultInfo.setMessage("异常");
		return resultInfo;
	}
}

相关问题

  • 为什么用JWT?
    • JWT只通过算法实现对Token合法性的验证,不依赖数据库,Memcached的等存储系统,因此可以做到跨服务器验证,只要密钥和算法相同,不同服务器程序生成的Token可以互相验证。
  • JWT Token不需要持久化在任何NoSQL中,不然背离其算法验证的初心
  • 在退出登录时怎样实现JWT Token失效呢?
    • 退出登录, 只要客户端端把Token丢弃就可以了,服务器端不需要废弃Token。
  • 怎样保持客户端长时间保持登录状态?
    • 服务器端提供刷新Token的接口, 客户端负责按一定的逻辑刷新服务器Token。
  • 服务器端是否应该从JWT中取出userid用于业务查询?
    • REST API是无状态的,意味着服务器端每次请求都是独立的,即不依赖以前请求的结果,因此也不应该依赖JWT token做业务查询, 应该在请求报文中单独加个userid 字段。
  • 为了做用户水平越权的检查,可以在业务层判断传入的userid和从JWT token中解析出的userid是否一致, 有些业务可能会允许查不同用户的数据。

开发流程

  • 常见验证流程:
    • 用户提交用户名、密码到服务器后台
    • 后台验证用户信息的正确性
    • 若用户验证通过,服务器端生成Token,返回到客户端
    • 客户端保存Token,再下一次请求资源时,附带上Token信息
    • 服务器端(一般在拦截器中进行拦截)验证Token是否由服务器签发的
    • 若Token验证通过,则返回需要的资源

源码

参考文章

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018-12-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 认证机制
    • 常见的几种认证机制
      • HTTP Basic Auth
      • OAuth(开放授权)
      • Cookie/Session 认证机制
      • 基于 Token 的认证机制
      • 有状态服务和无状态服务
    • 基于JWT(JSON WEB TOKEN)的Token认证机制实现
      • 头部(Header)
      • 载荷(Payload)
      • 签名(Signature)
    • JJWT
      • 添加依赖
      • 生成token
      • 解析token
      • 设置过期时间
      • 添加自定义属性
    • 在拦截器中配置
      • JWT工具类
      • 配置文件
      • 拦截器
      • 配置拦截器
      • 使用
    • 相关问题
      • 开发流程
        • 源码
          • 参考文章
          相关产品与服务
          内容分发网络 CDN
          内容分发网络(Content Delivery Network,CDN)通过将站点内容发布至遍布全球的海量加速节点,使其用户可就近获取所需内容,避免因网络拥堵、跨运营商、跨地域、跨境等因素带来的网络不稳定、访问延迟高等问题,有效提升下载速度、降低响应时间,提供流畅的用户体验。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档