从开始的cookie在web间做信息认证,到session机制,紧接着因为session带来的跨域问题,和越来越多的前后端分离、分布式项目,更多的开发者选择了自己喜爱的token机制,以及jwt(JSON web token),本文会从cookie开始,一步一步介绍到jwt的使用。
目录
session机制带来了什么
为什么要用token?
jwt使用。
jwt实战应用:
cookie 存储在浏览器中。 浏览器 js可以使用document.cookie获取cookie信息 。 相当于给访问用户提供一个通行证。 而session则相当于是在服务器上存储一份用户档案,不过相应服务器压力也会增加。cookie大小4k; cookie默认是在浏览器关闭后失效,也可以通过expires设置时间 保存在本地硬盘里;
cookie注入
有的网站没有对cookie做特殊处理,那么就可以线使用 xss获取到cookie, 然后用chrome上的插件cookieheacker进行注入绕过登录。
session 机制 : session是基于cookie的。 session保存在服务器上,cookie保存在客户端浏览器。 流程大致是,用户A从浏览器登录访问了服务器,服务器会在session 的map中存储key为session_id_A,value 为value_a的键值对,而用户B访问过该服务器后,也同样能存储,即session最后会保存为{session_id_A,value_a},:{session_id_B,value_b}...的map形式中。用户A在访问服务器后得到session_id_A后存储在浏览器中的cookie中,并且每次登录一次,就会将对应的session活跃一次。
session中保存的信息尽量精简,负责各个用户的信息都保存起来会很占用内存,大量请求造成内存泄露。
那假如说浏览器不支持cookie呢 ,很多手机上就不支持,而session是基于cookie的 ,怎么办? 这里就可以使用url地址重写。在重写的地址url后面添加sessionId.
session持久化 session保存在数据库中
使用session鸡肋: cookie和seesion必须是在同一域名下有效,为啥呢?就好比浏览器得到了百度的cookie_bd,又访问了谷歌,得到cookie_google。那谷歌可以去篡改百度域名下的cookie_bd吗,肯定不行 这就是他的不可跨域名性。跨域导致请求无法携带和服务器对应的cookie ,这也意味着你自己开发的项目前后端必须是在一台服务器上 ??? 说好的要搞前后端分离呢,所以session-cookie机制只能处理同源,对于不同域下的前后端就得想法子了,常见的nginx反向代理 、jsonp、CORS跨域资源共享,当然token也可以
在使用token时候,服务器只需要存储一个密钥, 而将随机的字符串token返回给浏览器,浏览器在localstorage /sessionstorage /cookie 中存储, 只需要存密钥不方便吗,真香
在需要向后台请求接口时,在请求头添加: authorization:token。因为可以不依赖cookie,就可以避免CSRF跨站伪造攻击 ,多用于restful api请求。
token的组成 : 加密信息+用户信息+生成签名 ,在jwt中(JSON web token)中形式为 header.payload.signature
token 认证过程
首次登陆,后端登陆认证成功后,生成token签名,随response返回给前端。前端将token 存储在localstorage中,前端页面在每次页面跳转时候,判断localstorage中是否有token,没有则定向到login页面,有则登陆成功。后台访问接口时候,后端根据token有无判断,以及是否过期,是否被篡改,返回200 或者401.前端获取到401时候重定向到登录页面。
是否被篡改,就需要根据服务器端保存的密钥进行判断。
jwt(json web token)常用于生成token,并包含对应的校验。下面进行详细介绍:
jwt 签发原理如下 ,
jwt总共包含三部分
header {"type":"JWT","alg":"HS256"} ; 载荷payload{"userId":021,"book":"月亮与六便士"};签名signature
第一部分:
使用base64对头部header编码,header包含了加密方式,这里是hs256,编码后为eyJ0eXBlIjoiSldUIiwiYWxnIjoiSFMyNTYifQ== 组成jwt的第一部分;
第二部分:
载荷payload包含token需要传达的信息,进行base64编码后为 eyJ1c2VySWQiOjAyMSwiYm9vayI6IuaciOS6ruS4juWFreS+v+WjqyJ9 组成jwt的第二部分 和 第一部分用. 分割开来。 因为base64编码后可以继续解码,所以载荷这里不推荐存储敏感信息,如密码信息等,负责容易解码泄露
第三部分:
加密后的信息,对前两部分的组合 eyJ0eXBlIjoiSldUIiwiYWxnIjoiSFMyNTYifQ==.eyJ1c2VySWQiOjAyMSwiYm9vayI6IuaciOS6ruS4juWFreS+v+WjqyJ9
使用头部定义的hs256加密,该加密算法是对称加密,服务器保存密钥key,这里也可以使用rs256非对称加密。加密后的结果为TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ ,该部分与起那两部分用.分割开 。
所以最后的token为eyJ0eXBlIjoiSldUIiwiYWxnIjoiSFMyNTYifQ==.eyJ1c2VySWQiOjAyMSwiYm9vayI6IuaciOS6ruS4juWFreS+v+WjqyJ9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
jwt验证过程
前面提到 token可以在用户登录认证成功后,随response信息返回前端。前端请求是放在http请求头部既可。
服务器在收到前端请求时候 ,对token验证 ,只需要一个密钥key即可,对头部和负载使用key进行加密,加密结果如果和签名signature一致,则说明token有效。
这里假设token被恶意篡改了,比如更改了载荷payload里的一个value,然后base64编码后又被放置在新的token中。服务器收到该更改后的token,使用密钥key进行token的前两个部分进行加密校验,会发现加密后的结果和被更改后的token上的签名不一致,就能发现token已经被篡改,这时响应返回401既可。
1、引入依赖
io.jsonwebtoken
jjwt
RELEASE
2、处理签名获取 token ,这里编写一个工具类 JwtTokenUtil
添加配置文件 bootstrap-dev.yml:
jwt:
secret: lile #密钥
expiration: 100000 #过期时间
package utils;
import domain.UserInfo;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Data;
import org.springframework.boot.autoconfigure.security.oauth2.resource.ResourceServerProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @ClassName JwtTokenUtil
* @Description 生成校验token
* @Author lile
* @Date 2020/2/15 14:27
* @Version 1.0
*/
@Data
@ConfigurationProperties(prefix = "jwt")
@Component
public class JwtTokenUtil {
private String secret;
// 过期时间 毫秒
private Long expiration;
/**
* 从数据声明生成令牌
*
* @param claims 数据声明
* @return 令牌
*/
private String generateToken(Map claims) {
Date expirationDate = new Date(System.currentTimeMillis() + expiration);
return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, secret).compact();
}
/**
* 从令牌中获取数据声明
*
* @param token 令牌
* @return 数据声明
*/
private Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}
/**
* 生成令牌
*
* @param userDetails 用户
* @return 令牌
*/
public String generateToken(UserDetails userDetails) {
Map claims = new HashMap<>(2);
claims.put(Claims.SUBJECT, userDetails.getUsername());
claims.put(Claims.ISSUED_AT, new Date());
return generateToken(claims);
}
/**
* 使用电话号生成令牌
* @param phone
* @return 令牌
*/
public String generateTokenByUsername(String phone){
Map claims = new HashMap<>(2);
claims.put(Claims.SUBJECT, phone);
claims.put(Claims.ISSUED_AT, new Date());
return generateToken(claims);
}
/**
* 从令牌中获取用户名
*
* @param token 令牌
* @return 用户名
*/
public String getUsernameFromToken(String token) {
String username;
try {
Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
/**
* 判断令牌是否过期
*
* @param token 令牌
* @return 是否过期
*/
public Boolean isTokenExpired(String token) {
try {
Claims claims = getClaimsFromToken(token);
Date expiration = claims.getExpiration();
return expiration.before(new Date());
} catch (Exception e) {
return true;
}
}
/**
* 刷新令牌
*
* @param token 原令牌
* @return 新令牌
*/
public String refreshToken(String token) {
String refreshedToken;
try {
Claims claims = getClaimsFromToken(token);
claims.put(Claims.ISSUED_AT, new Date());
refreshedToken = generateToken(claims);
} catch (Exception e) {
refreshedToken = null;
}
return refreshedToken;
}
/**
* 验证令牌
*
* @param token 令牌
* @param userDetails 用户
* @return 是否有效
*/
public Boolean validateToken(String token, UserDetails userDetails) {
UserInfo user = (UserInfo) userDetails;
String username = getUsernameFromToken(token);
return (username.equals(user.getUsername()) && !isTokenExpired(token));
}
}
在登录接口的中,如果登录成功,则在响应中添加token.
@ApiOperation(value = "用户登录")
@ApiResponses(
{@ApiResponse(code=200,message="成功",response = DynamicResponse.class)}
)
@PostMapping
@CrossOrigin
@ResponseBody
public DynamicResponse login (@RequestBody LoginRequest loginRequest){
return DynamicResponse.of((()->{
User user = userService.findUserByphone(loginRequest.getPhone());
Checker.checkNoNull(user,ErrorCode.NOT_USER.throwSupplier("用户不存在"));
Checker.checkTrue(PasswordUtil.sha256(user.getSalt(),loginRequest.getPwd()).equals(user.getPassword()),
ErrorCode.LOGIN_FAILURE.throwSupplier("密码不正确"));
// todo 处理token 封装res
String token = jwtTokenUtil.generateTokenByUsername(loginRequest.getPhone());
return user;
}));
}
3、增加注解 passtoken是绕过token验证; 另一个是必须要登录后才可以执行
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
/*
* @Author leli
* @Description //跳过token验证 * @Date 13:22 2020/2/10
* @Param
* @return
**/
public @interface PassToken {
boolean required() default true;
}
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
/*
* @Author leli
* @Description //需要登录才可以操作 * @Date 13:23 2020/2/10
* @Param
* @return
**/
@interface UserLoginToken {
boolean required() default true;
}
4、编写过滤器
package com.lile.handler;
import com.lile.common.mybits.model.User;
import com.lile.service.UserService;
import exceptions.UncheckedException;
import io.jsonwebtoken.JwtException;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import utils.ErrorCode;
import utils.JwtTokenUtil;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
/**
* @ClassName AuthenticationInterceptor
* @Description TODO
* @Author lile
* @Date 2020/2/10 13:48
* @Version 1.0
*/
public class AuthenticationInterceptor implements HandlerInterceptor {
@Resource
private UserService userService;
@Resource
private JwtTokenUtil jwtTokenUtil;
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
String token = httpServletRequest.getHeader("token");// 从 http 请求头中取出 token
// 如果不是映射到方法直接通过
if(!(object instanceof HandlerMethod)){
return true;
}
HandlerMethod handlerMethod=(HandlerMethod)object;
Method method=handlerMethod.getMethod();
//检查是否有passtoken注释,有则跳过认证
if (method.isAnnotationPresent(PassToken.class)) {
PassToken passToken = method.getAnnotation(PassToken.class);
if (passToken.required()) {
return true;
}
}
//检查有没有需要用户权限的注解
if (method.isAnnotationPresent(UserLoginToken.class)) {
UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class);
if (userLoginToken.required()) {
// 执行认证
if (token == null) {
throw new UncheckedException(ErrorCode.LOGIN_FAILURE,"无token,请重新登录");
}
// 获取 token 中的 phone ,phone唯一
String phone;
try {
phone = jwtTokenUtil.getUsernameFromToken(token);
} catch (JwtException j) {
throw new UncheckedException(ErrorCode.PERMISSION_DENIED,"token错误,权限不足");
}
User user = userService.getUserById(Integer.parseInt(phone));
if (user == null) {
throw new UncheckedException(ErrorCode.PERMISSION_DENIED,"用户不存在,重新登录");
}
// 验证 token
if(!jwtTokenUtil.isTokenExpired(token)&& jwtTokenUtil.generateTokenByUsername(phone).equals(token)){
throw new UncheckedException(ErrorCode.PERMISSION_DENIED,"token过期或不正确");
}
return true;
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
}
5、配置拦截器
package com.lile.config;
import com.lile.handler.AuthenticationInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
public class CorsConfig {
private CorsConfiguration buildConfig() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
return corsConfiguration;
}
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
// 配置所有请求
source.registerCorsConfiguration("/**", buildConfig());
return new CorsFilter(source);
}
@Bean
public AuthenticationInterceptor authenticationInterceptor() {
return new AuthenticationInterceptor();
}
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurerAdapter() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry
.addMapping("/**")
.allowedHeaders("*")
.allowedMethods("*")
.allowedOrigins("*");
}
// 使用上面定义的拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authenticationInterceptor())
.addPathPatterns("/**");
}
};
}
}
6、 应用
在需要登录才能执行的方法前面使用@UserloginToken
如下:
@RestController
public class HellowordController {
@UserLoginToken
@GetMapping("/hello")
public String index(){
return "hellow rod";
}
}
在swagger中试一下 请求该接口:
该接口会验证该request中是否包含正确的token 。
上面jwt是基于WebMvcConfigurer自定义过滤器进行安全控制,后面将介绍jwt和 spring security组合使用。