一、JWT令牌
1、什么是JWT令牌
JWT是JSON Web Token的缩写,即JSON Web令牌,是一种自包含令牌。
JWT的使用场景:
- 一种情况是webapi,类似之前的阿里云播放凭证的功能
- 另一种情况是多web服务器下实现无状态分布式身份验证
- JWT官网有一张图描述了JWT的认证过程
- JWT官网有一张图描述了JWT的认证过程
JWT的作用:
- JWT 最重要的作用就是对 token信息的防伪作用
JWT的原理:
- 一个JWT由三个部分组成:JWT头、有效载荷、签名哈希
- 最后由这三者组合进行base64编码得到JWT
2、JWT令牌的组成
典型的,一个JWT看起来如下图:
https://jwt.io/
该对象为一个很长的字符串,字符之间通过"."分隔符分为三个子串。
每一个子串表示了一个功能块,总共有以下三个部分:JWT头、有效载荷和签名
Base64URL算法
如前所述,JWT头和有效载荷序列化的算法都用到了Base64URL。该算法和常见Base64算法类似,稍有差别。
作为令牌的JWT可以放在URL中(例如api.example/?token=xxx)。 Base64中用的三个字符是"+","/“和”=",由于在URL中有特殊含义,因此Base64URL中对他们做了替换:"=“去掉,”+“用”-“替换,”/“用”_"替换,这就是Base64URL算法。
注意:base64编码,并不是加密,只是把明文信息变成了不可见的字符串。但是其实只要用一些工具就可以把base64编码解成明文,所以不要在JWT中放入涉及私密的信息。
3、JWT的用法
客户端接收服务器返回的JWT,将其存储在Cookie或localStorage中。
此后,客户端将在与服务器交互中都会带JWT。如果将它存储在Cookie中,就可以自动发送,但是不会跨域,因此一般是将它放入HTTP请求的Header Authorization字段中。
当跨域时,也可以将JWT放置于POST请求的数据主体中。
二、JWT问题和趋势
1、JWT默认不加密,但可以加密。生成原始令牌后,可以使用该令牌再次对其进行加密。
2、当JWT未加密时,一些私密数据无法通过JWT传输。
3、JWT不仅可用于认证,还可用于信息交换。善用JWT有助于减少服务器请求数据库的次数。
4、JWT的最大缺点是服务器不保存会话状态,所以在使用期间不可能取消令牌或更改令牌的权限。也就是说,一旦JWT签发,在有效期内将会一直有效。
5、JWT本身包含认证信息,因此一旦信息泄露,任何人都可以获得令牌的所有权限。为了减少盗用,JWT的有效期不宜设置太长。对于某些重要操作,用户在使用时应该每次都进行身份验证。
6、为了减少盗用和窃取,JWT不建议使用HTTP协议来传输代码,而是使用加密的HTTPS协议进行传输。
三、落地实现
1. 基本依赖
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
2.在application.yaml文件中添加jwt相关的配置
jwt:
secret: tuling-portal #服务端私钥
expiration: 86400 #过期时间 60*60*24 = 一天
tokenHead: Bearer #jwt规范 #告诉客户端jwt令牌开头需要加的一个字符串
tokenHeader: Authorization #告诉客户端你要在请求头里面传什么参数名字
secure:
ignored:
urls: #安全路径白名单
- /swagger-ui.html
- /swagger-resources/**
- /swagger/**
- /**/v2/api-docs
- /**/*.js
- /**/*.css
- /**/*.png
- /**/*.ico
- /webjars/springfox-swagger-ui/**
- /actuator/**
- /druid/**
- /error
- /login/**
- /register/**
- /user/**
3. 添加jwt加密解密 工具类
package cn.ecut.lrj.web.util;
import cn.ecut.lrj.web.uns.model.User;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* JwtToken生成的工具类
* JWT token的格式:header.payload.signature
* header的格式(算法、token的类型):
* {"alg": "HS512","type": "JWT"}
* payload的格式(用户名、创建时间、生成时间):
* {"sub":"wang","created":1489079981393,"exp":1489684781}
* signature的生成算法:
* HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
*/
@Component
public class JwtTokenUtil2 {
private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil2.class);
private static final String CLAIM_KEY_USERNAME = "user_name";
private static final String CLAIM_KEY_CREATED = "created";
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
@Value("${jwt.tokenHead}")
private String tokenHead;
@Value("${jwt.tokenHeader}")
private String tokenHeader;
/**
* 根据负责生成JWT的token
*/
private String generateToken(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate())
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/**
* 从token中获取JWT中的负载
*/
private Claims getClaimsFromToken(String token) {
Claims claims = null;
try {
claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
LOGGER.info("JWT格式验证失败:{}",token);
}
return claims;
}
/**
* 生成token的过期时间
*/
private Date generateExpirationDate() {
return new Date(System.currentTimeMillis() + expiration * 1000);
}
/**
* 解密: 从token中获取登录用户名(项目中使用)
*/
public String getUserNameFromToken(String token) {
String username;
try {
Claims claims = getClaimsFromToken(token);
username = claims.get(CLAIM_KEY_USERNAME,String.class);
} catch (Exception e) {
username = null;
}
return username;
}
/**
* 加密:根据用户信息生成token(项目中使用)
*/
public String generateToken(String userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put(CLAIM_KEY_USERNAME, userDetails);
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
/**
* 验证token是否还有效
*
* @param token 客户端传入的token
* @param userDetails 从数据库中查询出来的用户信息
*/
/*public boolean validateToken(String token, User userDetails) {
String username = getUserNameFromToken(token);
return username.equals(userDetails.getLoginAct()) && !isTokenExpired(token);
}*/
/**
* 判断token是否已经失效
*/
public boolean isTokenExpired(String token) {
Date expiredDate = getExpiredDateFromToken(token);
return expiredDate.before(new Date());
}
/**
* 从token中获取过期时间
*/
private Date getExpiredDateFromToken(String token) {
Claims claims = getClaimsFromToken(token);
return claims.getExpiration();
}
/**
* 判断token是否可以被刷新
*/
public boolean canRefresh(String token) {
return !isTokenExpired(token);
}
/**
* 刷新token
*/
public String refreshToken(String token) {
Claims claims = getClaimsFromToken(token);
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
}
4. 客户端发起登入请求
axios.post(
'http://localhost:8989/user/login',
{
"loginAct": this.user.loginAct,
'loginPwd': this.user.loginPwd
},{headers: {'Content-Type': 'application/json'}})
.then((res)=>{
})
5. 服务端验证成功后加密token,把token返回给客户端
在controller层验证登入成功后进行加密
package cn.ecut.lrj.web.uns.controller;
import cn.ecut.lrj.web.api.CommonResult;
import cn.ecut.lrj.web.uns.model.User;
import cn.ecut.lrj.web.uns.service.UserService;
import cn.ecut.lrj.web.util.JwtTokenUtil2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/user")
@CrossOrigin
public class UserController {
@Autowired
private UserService userService;
@Autowired
private JwtTokenUtil2 jwtTokenUtil2;
@Value("${jwt.tokenHead}")
private String tokenHead; //#jwt规范 #告诉客户端jwt令牌开头需要加的一个字符串
@Value("${jwt.tokenHeader}") //#告诉客户端你要在请求头里面传什么参数名字
private String tokenHeader;
@RequestMapping(value = "/login", method = RequestMethod.POST)
public CommonResult login(@RequestBody User user) {
User login = userService.login(user.getLoginAct(), user.getLoginPwd());
if (login == null) {
return CommonResult.validateFailed("用户名或密码错误");
}
//jwt加密
Map<String, String> tokenMap = new HashMap<>();
String token = jwtTokenUtil2.generateToken(login.getLoginAct());
tokenMap.put("token",token);
tokenMap.put("tokenHead",tokenHead);
tokenMap.put("tokenHeader",tokenHeader);
return CommonResult.success(tokenMap);
}
}
6.客户端接收拼接tokenHead+ jwt 存入cookie
axios.post(
'http://localhost:8989/user/login',
{
"loginAct": this.user.loginAct,
'loginPwd': this.user.loginPwd
},{headers: {'Content-Type': 'application/json'}})
.then((res)=>{
console.log(res.data.data)
let result = res.data.data
/* this.$cookie.set('token',result.tokenHead+' '+result.token,{expires:'1M'});*/
setCookie("token",result.tokenHead+' '+result.token,{expires:'1M'}) //cookie自己定义(安装一个cookie库,或定义)
// 拿到payloader 解码
var tokenStr= decodeURIComponent(escape(window.atob(result.token.split('.')[1])));
// 转换为json对象
let username = JSON.parse(tokenStr).user_name;
setCookie("username",username,120);
this.$router.push("/success")
})
setcookie(),getcookie()定义
export function setCookie(name,value)
{
var Days = 30;
var exp = new Date();
exp.setTime(exp.getTime() + Days*24*60*60*1000);
document.cookie = name + "="+ escape (value) + ";expires=" + exp.toGMTString();
}
export function getCookie(name)
{
var arr,reg=new RegExp("(^| )"+name+"=([^;]*)(;|$)");
return (arr=document.cookie.match(reg))?unescape(arr[2]):null;
}
export function delCookie(name)
{
var exp = new Date();
exp.setTime(exp.getTime() - 1);
var cval=getCookie(name);
if(cval!=null)
document.cookie= name + "="+cval+";expires="+exp.toGMTString();
}
7. 后续客户端所有的axios请求都需要携带cookie中token放入请求头Authorization中
// axios的拦截器 jwt+spring security
axios.interceptors.request.use(config => {
// jwt令牌
var token= getCookie("token");
window.console.log(token);
if (token !=undefined) {
config.headers['Authorization'] = token; // 让每个请求携带自定义token 请根据实际情况自行修改
}
return config
}, error => {
// Do something with request error
Promise.reject(error)
})
8. 服务器拦截到请求头中的Authorization 解密jwt .从而拿到username 查询 是否存在用户
- 在拦截器中进行验证token是否正确、失效
- 拦截器配置文件
package cn.ecut.lrj.web.config;
import cn.ecut.lrj.web.Interceptor.AuthInterceptor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class AuthInterceptorConfig implements WebMvcConfigurer {
/**
* 该拦截器主要是为了权限验证
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor()).addPathPatterns("/**");
}
/**
*
* @return
*/
@Bean
@ConfigurationProperties(prefix = "secure.ignored")//获取配置文件中的白名单urls
public AuthInterceptor authInterceptor(){
return new AuthInterceptor();
}
}
- 拦截器
package cn.ecut.lrj.web.Interceptor;
import cn.ecut.lrj.web.api.ResultCode;
import cn.ecut.lrj.web.exception.ApiException;
import cn.ecut.lrj.web.uns.model.User;
import cn.ecut.lrj.web.uns.service.UserService;
import cn.ecut.lrj.web.util.JwtTokenUtil2;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
@Slf4j
public class AuthInterceptor implements HandlerInterceptor {
private List<String> urls;
@Autowired
private JwtTokenUtil2 jwtTokenUtil2;
@Autowired
private UserService userService;
@Value("${jwt.tokenHead}")
private String tokenHead;
@Value("${jwt.tokenHeader}")
private String tokenHeader;
public List<String> getUrls() {
return urls;
}
public void setUrls(List<String> urls) {
this.urls = urls;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1、不需要登录就可以访问的路径——白名单
// 获取当前请求 /admin/login
log.info("拦截开始");
String requestURI = request.getRequestURI();
log.info("requestURI-->"+requestURI);
// Ant方式路径匹配 /** ? _
PathMatcher matcher = new AntPathMatcher();
for (String ignoredUrl : urls) {
if(matcher.match(ignoredUrl,requestURI)){
return true;
}
}
//拿到jwt令牌
String jwt = request.getHeader(tokenHeader);
System.out.println("jwt令牌: "+jwt);
//判断是否存在 判断开头是否加了tokenHead
if (StrUtil.isBlank(jwt) || !jwt.startsWith(tokenHead)){
throw new ApiException(ResultCode.UNAUTHORIZED);
}
//存在就进行解密
jwt=jwt.substring(tokenHead.length());
String userName = jwtTokenUtil2.getUserNameFromToken(jwt);
if (StrUtil.isBlank(userName)){
throw new ApiException(ResultCode.UNAUTHORIZED);
}
/*//将userName存入ThreadLocal中(方便下面获取当前用户)
JwtTokenUtil2.currentMember.set(userName);*/
//得到userName就进行查询用户是否存在和 token是否过期
User member = userService.getAdminByUsername(userName);
boolean result = jwtTokenUtil2.isTokenExpired(jwt);
if (!result){
throw new ApiException(ResultCode.UNAUTHORIZED);
}
return true;
}
}