Token认证

文章目录

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对象。
{
    "typ": "JWT",
    "alg": "HS256"
}

载荷(Payload)

  • iss: 该JWT的签发者,是否使用是可选的;
  • sub: 该JWT所面向的用户,一般是用户名,是否使用是可选的;
  • aud: 接收该JWT的一方,是否使用是可选的;
  • exp(expires): 什么时候过期,这里是一个Unix时间戳,是否使用是可选的;
  • iat(issued at): 在什么时候签发的(UNIX时间),一般是登录时间,是否使用是可选的; 其他还有:
  • nbf (Not Before):如果当前时间在nbf里的时间之前,则Token不被接受;一般都会留一些余地,比如几分钟;,是否使用是可选的;
{ "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生成

添加依赖

<!-- jjwt -->
<dependency>
	<groupId>io.jsonwebtoken</groupId>
	<artifactId>jjwt</artifactId>
	<version>0.9.0</version>
</dependency>

生成token

/**
 *  	生成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需要知道秘钥
/*
	 * 	解析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);
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作为自定义的属性
/**
	 *  	生成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工具类

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();
	}
}

配置文件

jwt: # JWT的配置
  config:
    secret: secret   ## 秘钥
    expire: 3600000  ## 过期时间1个小时

拦截器

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;
	}
}

配置拦截器

  • 一定要先注入拦截器类,否则拦截器内的其他对象将不能注入
@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
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中的角色验证码(后期可以使用切面将其提取出来)
/**
 *   删除用户
 * @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验证通过,则返回需要的资源

源码

参考文章

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 性能场景之网络模拟

    TC(traffic control)是Linux中的流量控制工具。它是通过控制netem来实现的网络场景模拟。该工具是直接对物理网卡生效的,如果是逻辑网卡,则...

    小老鼠
  • SpringSecurity详细介绍RememberMe功能

      先测试一下,认证通过后,关掉浏览器,再次打开页面,发现还要认证!为什么没有起作用呢? 这是因为remember me功能使用的过滤器RememberMeAu...

    用户4919348
  • 部署YApi可视化接口管理平台

    这个时候提示,在浏览器中打开 http://0.0.0.0:9090访问。……ip换成自己服务器的

    华创信息技术
  • 单点登录SSO解决方案之SpringSecurity+JWT实现

      通过前面几天文章我们详细的介绍了SpringSecurity的使用,本文我们来看下,结合JWT来实现单点登录操作。

    用户4919348
  • 基于Django的电子商务网站开发(连载21)

    商品概要信息的分页显示页面是登录操作以后的首界面,以列表的形式显示已经存在的商品,通过这个页面,用户可以进行查看商品信息的详情、添加商品进入购物车等操作。

    小老鼠
  • SpringSecurity常用过滤器介绍

      首当其冲的一个过滤器,非常重要 主要是使用SecurityContextRepository在session中保存或更新一个SecurityContext...

    用户4919348
  • 基于Django的电子商务网站开发(连载20)

    商品信息模块包括“商品信息的维护”“商品概要信息的分页显示”“根据商品名称的模糊查询”和“对某一条商品显示其详细信息”。商品信息的维护通过Django提供的后台...

    小老鼠
  • 什么是CC攻击?如何有效防御?

    CC攻击其实属于DDoS攻击的一种,其原理就是攻击者控制某些主机不停地发大量数据包给对方服务器造成服务器资源耗尽,一直到宕机崩溃。这种攻击普遍都是流量不是很高,...

    墨者安全科技
  • 测试之物竟天择适者生存

    测试人员的洗牌已经有苗头了,测试行业也是物竟天择,适者生存!你需要了解 一下相关内容,时刻保持危机意识!

    小老鼠

扫码关注云+社区

领取腾讯云代金券